Skip to content

v1.11.0#51

Merged
mattyhansen merged 16 commits into
mainfrom
dev
Jun 10, 2026
Merged

v1.11.0#51
mattyhansen merged 16 commits into
mainfrom
dev

Conversation

@mattyhansen

@mattyhansen mattyhansen commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

This pull request simplifies and improves the reliability of policy hook resolution logic across several configuration files and test scripts. The main focus is on removing legacy logic for determining the repository root (especially for submodules and alternate worktrees), standardizing error handling, and ensuring that policy checks correctly use the active worktree's policy store. It also updates documentation for clarity.

Policy hook resolution and error handling:

  • Simplified the logic for resolving the git repository root in .goat-flow/hooks/deny-dangerous.sh by removing the git-common-dir-based resolution and always using git rev-parse --show-toplevel, ensuring the active worktree's policy store is used. (.goat-flow/hooks/deny-dangerous.sh)
  • Updated all hook command invocations in .codex/hooks.json, .agents/hooks.json, and .claude/settings.json to use the new simplified root resolution logic, with standardized error and skip messages for missing policy hooks. (.codex/hooks.json, .agents/hooks.json, .claude/settings.json) [1] [2] [3] [4] [5] [6] [7] [8]

Test improvements:

  • Refactored test cases in .goat-flow/hooks/deny-dangerous/deny-dangerous-self-test.sh to match the new resolution logic, removing tests for git-common-dir and adding new tests that verify the active worktree's policy store is used, including for real linked worktrees. (.goat-flow/hooks/deny-dangerous/deny-dangerous-self-test.sh) [1] [2] [3] [4] [5]

Documentation updates:

  • Clarified the description of agent role-play in multi-perspective critique documentation, replacing "SBAO" with explicit "SKEPTIC/ANALYST/STRATEGIST" for better understanding. (.github/skills/goat-critique/SKILL.md, .claude/skills/goat-critique/SKILL.md, .agents/skills/goat-critique/SKILL.md)

These changes improve maintainability, ensure correct policy enforcement in complex git worktree scenarios, and provide clearer documentation for users and developers.


Note

Medium Risk
Changes affect PreToolUse/PostToolUse policy hooks and command-blocking rules (agent safety surface), but are harness-focused with expanded self-tests rather than production app logic.

Overview
v1.11.0 bumps the harness and mirrored agent skills from 1.10.1 to 1.11.0, with the largest behavioral changes in policy hooks and learning-loop retrieval.

Hooks: Pre/post-tool hook launchers in .agents, .claude, .codex, and .github now resolve the repo with git rev-parse --show-toplevel instead of git-common-dir heuristics, so linked worktrees use that worktree’s .goat-flow policy store. gruff-code-quality missing-hook handling skips (exit 0) with a stderr note instead of blocking edits; timeout rises to 90s. deny-dangerous drops submodule-specific root logic; tests add real linked-worktree coverage. Shell/write guards tighten piped xargs/rm, xargs -a/--arg-file, and piped git commit / reset / clean; backtick-substitution messaging is clearer.

gruff-code-quality.sh: Default timeout 60s, per-language GRUFF_*_BIN / GRUFF_*_TIMEOUT_SECONDS, stderr when config exists but the binary is missing, safer payload path filtering (no git-wide scan when the edit names only non-analyzable paths), and richer JSON config-error handling. ADR-032 documents explicit env overrides for monorepo layouts.

Learning loop: Skills and Copilot instructions switch from grep-first buckets to INDEX-first reads of generated INDEX.md files; goat-review footgun cross-check targets footguns/INDEX.md. ADR-035 and regenerated decisions/INDEX.md (and related index machinery in the diff) formalize goat-flow index + index-fresh staleness checks. Minor goat-critique doc fix: SKEPTIC/ANALYST/STRATEGIST naming.

Docs/config: .github/copilot-instructions.md adds Small Feature complexity budget and updates stated audit check counts; config.yaml version aligns with 1.11.0.

Reviewed by Cursor Bugbot for commit 1d2bdc1. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features

    • Added environment variable overrides for per-language code analyzer binary discovery and timeouts.
    • Enhanced detection of destructive commands in piped workflows.
  • Bug Fixes

    • Improved git repository root resolution for nested and linked worktree scenarios.
    • Fixed code quality hook to skip gracefully when configured analyzers are unavailable.
    • Enhanced error diagnostics for missing or misconfigured analyzers.
  • Improvements

    • Simplified hook command execution across multiple repository contexts.
    • Refined payload file filtering to focus on relevant analysis targets.
  • Documentation

    • Updated guidance for hook deployment and worktree compatibility.
    • Enhanced learning-loop documentation with new footgun and lesson entries.

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR updates hook launcher root resolution, deny-dangerous and gruff-code-quality behavior, dashboard Alpine fragment structure, and several audit/tooling helpers and tests.

Changes

Hook launchers and policy checks

Layer / File(s) Summary
Root resolution and launcher wiring
.agents/hooks.json, .claude/settings.json, .codex/hooks.json, workflow/hooks/agent-config/*, workflow/install-goat-flow.sh
Hook commands and generated launchers now resolve the repository root with git rev-parse --show-toplevel, with agent-specific fallback and unavailable-script handling preserved per agent.
deny-dangerous root and pattern checks
.goat-flow/hooks/deny-dangerous.sh, .goat-flow/hooks/deny-dangerous/*, workflow/hooks/deny-dangerous*, .goat-flow/learning-loop/footguns/deny-dangerous.md, workflow/skills/goat-critique/SKILL.md, CHANGELOG.md
The deny hook now resolves the active worktree root directly and scans pipeline segments and xargs payloads for destructive commands, with matching self-tests, docs, and notes updated.
gruff analyzer discovery and diagnostics
.goat-flow/hooks/gruff-code-quality.sh, workflow/hooks/gruff-code-quality.sh, .gruff-ts.yaml, ADR/docs, CHANGELOG.md, related lessons/footguns/tests
The gruff hook now supports per-language binary and timeout overrides, payload filtering, and clearer config/missing-binary diagnostics, with matching configuration, docs, and smoke coverage.

Dashboard state, prompts, and terminal fragments

Layer / File(s) Summary
Dashboard state and loading helpers
src/dashboard/dashboard-app-state-fragments.ts, src/dashboard/dashboard-app-data-loading-fragments.ts, src/dashboard/dashboard-prompts.ts, src/dashboard/dashboard-model-readers.ts, src/dashboard/dashboard-readers.ts, src/dashboard/app.ts, test/unit/dashboard-*, test/smoke/dashboard-endpoints.test.ts
Dashboard state derivation, data loading, prompt badges, and readers were split into smaller helpers and renamed fragment builders, with tests updated to match the new control flow and field handling.
Prompt, audit, and terminal helpers
src/dashboard/dashboard-app-prompts-audit-fragments.ts, src/dashboard/dashboard-app-project-terminal-fragments.ts, src/dashboard/dashboard-app-init.ts, src/dashboard/dashboard-app-skill-quality-fragments.ts, test/unit/dashboard-terminal-launch/*, test/unit/hook-registrar.test.ts
Prompt image upload, audit routing, terminal export/launch, xterm warmup/refit, and skill-quality fragment logic were extracted into helpers and rewired through renamed fragment factories.
Dashboard routes and readers
src/cli/server/dashboard-audit-routes.ts, src/cli/server/safe-exec.ts
Dashboard audit/setup routing and safe execution were refactored into helper-based implementations with centralized response and timeout handling.

CLI audits, filesystem helpers, and parity scripts

Layer / File(s) Summary
Audit context and filesystem helpers
src/cli/audit/check-agent-deny-mechanism.ts, src/cli/audit/harness/check-context.ts, src/cli/facts/fs.ts, src/cli/server/agent-hook-writer.ts
Audit deny checks, doc-path validation, filesystem caching, and hook command generation were split into helper-based implementations.
Parity and smoke scripts
scripts/check-instruction-parity.mjs, scripts/preflight-checks.sh, scripts/profile-dashboard-audit.mjs
Instruction parity, hook smoke, and CLI profiling scripts now use smaller dispatch helpers and run configured commands from multiple working directories.
Test fixtures and import rewiring
test/src.ts, test/fixtures/projects/index.ts, test/*
Test imports were consolidated through a facade module, and fixture/test comments and assertions were updated for the refactored helpers and launcher behavior.

Sequence Diagram(s)

sequenceDiagram
  participant HookConfig as Hook config
  participant Git as git rev-parse
  participant Launcher as hook launcher
  participant HookScript as .goat-flow/hooks/*.sh

  HookConfig->>Git: git rev-parse --show-toplevel
  Git-->>HookConfig: repo root
  HookConfig->>Launcher: cd root && run hook script
  Launcher->>HookScript: execute deny-dangerous.sh or gruff-code-quality.sh
Loading

Estimated review effort

🎯 5 (Critical) | ⏱️ ~90+ minutes

Possibly related PRs

  • blundergoat/goat-flow#49 — Both PRs change gruff-code-quality.sh control flow, including hook --capabilities, JSON handling, and contract execution.
  • blundergoat/goat-flow#47 — Both PRs adjust deny-dangerous launcher/root resolution and related hook generation behavior.

Poem

🐇 Hop hop, the hooks now know the way,
Through roots and worktrees, bright and gray.
I nibble xargs, skip the bad,
And gruffly grin at paths well-had.
A dashboard bun with tidy threads,
Then carrots for the review ahead!

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 706c71acb3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

root="$(repo_root)"
cd "$root" || exit 0
payload_paths="$(payload_file_paths "$payload")"
payload_paths="$(payload_supported_file_paths "$root" "$payload")"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep gruff scoped when payload paths are unsupported

When a PostToolUse payload names a real but unsupported file such as README.md, this now filters it out before deciding whether to fall back, so payload_paths becomes empty and the hook scans every git-changed supported file instead of doing nothing for that edit. I reproduced this with a pending src/a.ts change and an Edit payload for README.md; the hook reported the TypeScript finding even though the edited file was not analyzable. This makes ordinary docs/config edits surface stale/unrelated gruff diagnostics whenever there are pending code changes; distinguish “payload had paths but none supported” from “payload had no paths” before using git_changed_supported_paths.

Useful? React with 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR updates GOAT Flow’s hook execution model to consistently resolve the active worktree root (via git rev-parse --show-toplevel), tightens deny-dangerous pipeline detection (notably xargs-based destructive payloads), and refactors several dashboard/server/test utilities to improve reliability and maintainability.

Changes:

  • Standardize root-resolving hook launcher commands (Claude/Codex/Antigravity) and soften optional gruff-code-quality missing-hook behavior.
  • Expand deny-dangerous write/destructive detection to cover pipeline segments (xargs, piped git operations) and add/adjust self-tests.
  • Refactor dashboard app fragments/readers and server route/execution helpers; add a test-only test/src.ts facade to reduce deep relative imports.

Reviewed changes

Copilot reviewed 78 out of 78 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
workflow/skills/goat-critique/SKILL.md Docs wording update for critique “role-play” labels.
workflow/install-goat-flow.sh Installer hook launcher generation and messaging changes.
workflow/hooks/README.md Document updated hook root-resolution/runtime contracts.
workflow/hooks/deny-dangerous/patterns-writes.sh Update git write detection to scan pipeline segments + xargs.
workflow/hooks/deny-dangerous/patterns-shell.sh Detect destructive xargs rm -r payloads across pipeline segments.
workflow/hooks/deny-dangerous/deny-dangerous-self-test.sh Update resolver tests; add real linked-worktree + new pipeline cases.
workflow/hooks/deny-dangerous.sh Simplify git root resolution to --show-toplevel.
workflow/hooks/agent-config/codex-hooks.json Codex template updated to git-root wrapper launcher.
workflow/hooks/agent-config/claude.json Claude template updated to git-root wrapper launcher + fallback.
workflow/hooks/agent-config/antigravity-hooks.json Antigravity template updated to git-root wrapper launcher + deny JSON.
test/unit/terminal-uploads.test.ts Add fixture-purpose comment for symlink escape test.
test/unit/skill-quality/main.test.ts Add fixture-purpose comment for symlink walk-root filtering.
test/unit/skill-quality/helpers.ts Switch tests to import via test/src.ts facade.
test/unit/local-paths.test.ts Add fixture-purpose comment for symlink escape test.
test/unit/hook-registrar.test.ts Expand launcher assertions (Codex + gruff) and worktree expectations.
test/unit/dashboard-terminal-launch/launch-flow-06.test.ts Update static assertions to match extracted helper functions.
test/unit/dashboard-terminal-launch/fakes.ts Refactor deterministic fake timers into clearer helper functions/types.
test/unit/dashboard-reporting.test.ts Doc/comment tweaks + fixture-purpose comment.
test/unit/dashboard-readers.test.ts Strengthen assertions by requiring non-null enforcement block.
test/unit/dashboard-home.test.ts Add doc comment + fixture-purpose comment.
test/unit/config-writer.test.ts Add fixture-purpose comment for canonical hook id behavior.
test/unit/check-content-quality.test.ts Replace magic number with named constant.
test/unit/audit-harness/check-evidence-before-claims.test.ts Use test/src.ts facade for imports.
test/unit/audit-command/main.test.ts Use test/src.ts facade; consolidate imports.
test/unit/audit-command/hook-facts.test.ts Use test/src.ts facade; add launcher normalization test.
test/unit/audit-command/helpers.ts Use test/src.ts facade; consolidate imports/types.
test/unit/audit-command/agent-deny-hooks.test.ts Add nested-cwd failure test + error message refinement.
test/unit/agent-config-template-parity.test.ts Refine assertions; detect unknown deny rules more cleanly.
test/src.ts New test-only export facade for stable shallow imports.
test/smoke/dashboard-endpoints.test.ts Minor TS cast cleanup + comments.
test/integration/skill-author.test.ts Add fixture-purpose comment.
test/integration/setup-install.test.ts Add fixture-purpose comments for migration coverage.
test/integration/setup-install-codex-config-migration.test.ts Add fixture-purpose comments for migration coverage.
test/integration/path-integrity.test.ts Add fixture-purpose comments for router/copath validations.
test/integration/audit-drift-checkdrift-hook-templates.test.ts Add fixture-purpose comment for optional Copilot hooks allowlist.
test/fixtures/projects/index.ts Use test/src.ts facade for shared types/constants.
src/dashboard/dashboard-readers.ts Refactor audit-check provenance/enum decoding for stricter contracts.
src/dashboard/dashboard-prompts.ts Refactor preset badge construction into rule list helper.
src/dashboard/dashboard-model-readers.ts Refactor optional boolean/costTier decoding helpers.
src/dashboard/dashboard-app-state-fragments.ts Split/rename Alpine fragments; extract terminal-state helpers.
src/dashboard/dashboard-app-skill-quality-fragments.ts Rename fragments for clarity.
src/dashboard/dashboard-app-prompts-audit-fragments.ts Extract audit/upload helpers; split fragment responsibilities.
src/dashboard/dashboard-app-project-terminal-fragments.ts Extract skill evaluator + terminal scrollback helpers; split fragments.
src/dashboard/dashboard-app-init.ts Extract xterm warmup/refit helpers; simplify watchers.
src/dashboard/app.ts Update fragment composition to new fragment names/splits.
src/cli/server/safe-exec.ts Refactor exec validation/runtime config/output capture into helpers.
src/cli/server/dashboard-audit-routes.ts Refactor into smaller route-handler builders/helpers.
src/cli/server/agent-hook-writer.ts Update hook command generation to active-worktree root wrappers.
src/cli/facts/fs.ts Refactor createFS into smaller cached helper constructors.
src/cli/audit/harness/check-context.ts Refactor doc-path resolution into clearer helpers/accumulators.
src/cli/audit/check-agent-deny-mechanism.ts Refactor hook syntax checks; replay configured hooks from nested cwd too.
scripts/profile-dashboard-audit.mjs Refactor CLI arg parsing into handler map helpers.
scripts/preflight-checks.sh Run configured hook smoke checks from project root + nested .goat-flow.
scripts/check-instruction-parity.mjs Refactor parity validation into per-file helper functions.
CHANGELOG.md Add v1.10.2 entry + reword v1.10.1 bullets for clarity.
.gruff-ts.yaml Update rule set/thresholds and expand security/test-quality rule list.
.goat-flow/learning-loop/lessons/verification.md Update last_reviewed date.
.goat-flow/learning-loop/lessons/hook-testing.md Update last_reviewed + adjust Codex launcher lesson guidance.
.goat-flow/learning-loop/lessons/gruff-cleanup.md Update last_reviewed + add several new gruff/dashboard lessons.
.goat-flow/learning-loop/lessons/dashboard-testing.md Update evidence anchor text to match refactor.
.goat-flow/learning-loop/footguns/hooks.md Update last_reviewed + revise hook launcher footguns.
.goat-flow/learning-loop/footguns/deny-dangerous.md Update last_reviewed + add pipeline-segment inspection footgun.
.goat-flow/learning-loop/footguns/auditor.md Update evidence anchor wording for fs adapter comments.
.goat-flow/learning-loop/decisions/ADR-032-scope-gruff-hook-binary-discovery.md Add update note + document explicit per-language binary overrides.
.goat-flow/hooks/deny-dangerous/patterns-writes.sh Mirror writes-policy pipeline scanning changes into shipped hook copy.
.goat-flow/hooks/deny-dangerous/patterns-shell.sh Mirror destructive xargs pipeline scanning into shipped hook copy.
.goat-flow/hooks/deny-dangerous/deny-dangerous-self-test.sh Mirror updated self-tests into shipped hook copy.
.goat-flow/hooks/deny-dangerous.sh Mirror simplified git root resolution into shipped hook copy.
.github/skills/goat-critique/SKILL.md Docs wording update for critique “role-play” labels.
.codex/hooks.json Update repo’s Codex hook config to use git-root wrapper launcher.
.claude/skills/goat-critique/SKILL.md Docs wording update for critique “role-play” labels.
.claude/settings.json Update repo’s Claude hook config to use git-root wrapper launcher + gruff soft-fail.
.agents/skills/goat-critique/SKILL.md Docs wording update for critique “role-play” labels.
.agents/hooks.json Update repo’s Antigravity hook config to use git-root wrapper launcher + gruff soft-fail.

Comment on lines +745 to +749
const claudeRootFallback =
agent === "codex"
? ""
: `; [ -f "$root/.goat-flow/hooks/${script}" ] || root="\${CLAUDE_PROJECT_DIR:-}"`;
return `bash -c 'root="$(git rev-parse --show-toplevel 2>/dev/null || true)"${claudeRootFallback}; [ -f "$root/.goat-flow/hooks/${script}" ] || ${unavailable}; cd "$root" || ${unavailable}; bash "$root/.goat-flow/hooks/${script}"'`;
Comment on lines +140 to +144
const claudeRootFallback =
agent.id === "codex"
? ""
: `; [ -f "$root/${path}" ] || root="\${CLAUDE_PROJECT_DIR:-}"`;
const ensureRoot = `[ -f "$root/${path}" ] || ${unavailable}`;
{
"type": "command",
"command": ".goat-flow/hooks/deny-dangerous.sh",
"command": "bash -c 'root=\"$(git rev-parse --show-toplevel 2>/dev/null || true)\"; [ -f \"$root/.goat-flow/hooks/deny-dangerous.sh\" ] || { printf '\\''BLOCKED: Policy hook unavailable: git repository root unavailable.\\n'\\'' >&2; exit 2; }; cd \"$root\" || { printf '\\''BLOCKED: Policy hook unavailable: git repository root unavailable.\\n'\\'' >&2; exit 2; }; bash \"$root/.goat-flow/hooks/deny-dangerous.sh\"'",
{
"type": "command",
"command": "bash -c 'gcd=\"$(git rev-parse --git-common-dir 2>/dev/null)\"; root=\"\"; case \"$gcd\" in */.git/modules/*|.git/modules/*) root=\"$(git rev-parse --show-toplevel 2>/dev/null || true)\" ;; /*|[A-Za-z]:/*|[A-Za-z]:\\\\*) gcd=\"${gcd//\\\\//}\"; root=\"$(dirname \"$gcd\")\" ;; *) root=\"$(git rev-parse --show-toplevel 2>/dev/null || true)\" ;; esac; [ -f \"$root/.goat-flow/hooks/deny-dangerous.sh\" ] || root=\"${CLAUDE_PROJECT_DIR:-}\"; [ -f \"$root/.goat-flow/hooks/deny-dangerous.sh\" ] || { printf '\\''BLOCKED: Policy hook unavailable: git repository root unavailable.\\n'\\'' >&2; exit 2; }; cd \"$root\" || { printf '\\''BLOCKED: Policy hook unavailable: git repository root unavailable.\\n'\\'' >&2; exit 2; }; bash \"$root/.goat-flow/hooks/deny-dangerous.sh\"'"
"command": "bash -c 'root=\"$(git rev-parse --show-toplevel 2>/dev/null || true)\"; [ -f \"$root/.goat-flow/hooks/deny-dangerous.sh\" ] || root=\"${CLAUDE_PROJECT_DIR:-}\"; [ -f \"$root/.goat-flow/hooks/deny-dangerous.sh\" ] || { printf '\\''BLOCKED: Policy hook unavailable: git repository root unavailable.\\n'\\'' >&2; exit 2; }; cd \"$root\" || { printf '\\''BLOCKED: Policy hook unavailable: git repository root unavailable.\\n'\\'' >&2; exit 2; }; bash \"$root/.goat-flow/hooks/deny-dangerous.sh\"'"
{
"type": "command",
"command": "bash -c 'gcd=\"$(git rev-parse --git-common-dir 2>/dev/null)\"; root=\"\"; case \"$gcd\" in */.git/modules/*|.git/modules/*) root=\"$(git rev-parse --show-toplevel 2>/dev/null || true)\" ;; /*|[A-Za-z]:/*|[A-Za-z]:\\\\*) gcd=\"${gcd//\\\\//}\"; root=\"$(dirname \"$gcd\")\" ;; *) root=\"$(git rev-parse --show-toplevel 2>/dev/null || true)\" ;; esac; [ -f \"$root/.goat-flow/hooks/deny-dangerous.sh\" ] || root=\"${CLAUDE_PROJECT_DIR:-}\"; [ -f \"$root/.goat-flow/hooks/deny-dangerous.sh\" ] || { printf '\\''{\"decision\":\"deny\",\"reason\":\"Policy hook unavailable: git repository root unavailable.\"}\\n'\\''; exit 0; }; cd \"$root\" || { printf '\\''{\"decision\":\"deny\",\"reason\":\"Policy hook unavailable: git repository root unavailable.\"}\\n'\\''; exit 0; }; bash \"$root/.goat-flow/hooks/deny-dangerous.sh\"'",
"command": "bash -c 'root=\"$(git rev-parse --show-toplevel 2>/dev/null || true)\"; [ -f \"$root/.goat-flow/hooks/deny-dangerous.sh\" ] || root=\"${CLAUDE_PROJECT_DIR:-}\"; [ -f \"$root/.goat-flow/hooks/deny-dangerous.sh\" ] || { printf '\\''{\"decision\":\"deny\",\"reason\":\"Policy hook unavailable: git repository root unavailable.\"}\\n'\\''; exit 0; }; cd \"$root\" || { printf '\\''{\"decision\":\"deny\",\"reason\":\"Policy hook unavailable: git repository root unavailable.\"}\\n'\\''; exit 0; }; bash \"$root/.goat-flow/hooks/deny-dangerous.sh\"'",
Comment thread .codex/hooks.json
{
"type": "command",
"command": ".goat-flow/hooks/deny-dangerous.sh",
"command": "bash -c 'root=\"$(git rev-parse --show-toplevel 2>/dev/null || true)\"; [ -f \"$root/.goat-flow/hooks/deny-dangerous.sh\" ] || { printf '\\''BLOCKED: Policy hook unavailable: git repository root unavailable.\\n'\\'' >&2; exit 2; }; cd \"$root\" || { printf '\\''BLOCKED: Policy hook unavailable: git repository root unavailable.\\n'\\'' >&2; exit 2; }; bash \"$root/.goat-flow/hooks/deny-dangerous.sh\"'",
Comment thread .claude/settings.json
{
"type": "command",
"command": "bash -c 'gcd=\"$(git rev-parse --git-common-dir 2>/dev/null)\"; root=\"\"; case \"$gcd\" in */.git/modules/*|.git/modules/*) root=\"$(git rev-parse --show-toplevel 2>/dev/null || true)\" ;; /*|[A-Za-z]:/*|[A-Za-z]:\\\\*) gcd=\"${gcd//\\\\//}\"; root=\"$(dirname \"$gcd\")\" ;; *) root=\"$(git rev-parse --show-toplevel 2>/dev/null || true)\" ;; esac; [ -f \"$root/.goat-flow/hooks/deny-dangerous.sh\" ] || root=\"${CLAUDE_PROJECT_DIR:-}\"; [ -f \"$root/.goat-flow/hooks/deny-dangerous.sh\" ] || { printf '\\''BLOCKED: Policy hook unavailable: git repository root unavailable.\\n'\\'' >&2; exit 2; }; cd \"$root\" || { printf '\\''BLOCKED: Policy hook unavailable: git repository root unavailable.\\n'\\'' >&2; exit 2; }; bash \"$root/.goat-flow/hooks/deny-dangerous.sh\"'"
"command": "bash -c 'root=\"$(git rev-parse --show-toplevel 2>/dev/null || true)\"; [ -f \"$root/.goat-flow/hooks/deny-dangerous.sh\" ] || root=\"${CLAUDE_PROJECT_DIR:-}\"; [ -f \"$root/.goat-flow/hooks/deny-dangerous.sh\" ] || { printf '\\''BLOCKED: Policy hook unavailable: git repository root unavailable.\\n'\\'' >&2; exit 2; }; cd \"$root\" || { printf '\\''BLOCKED: Policy hook unavailable: git repository root unavailable.\\n'\\'' >&2; exit 2; }; bash \"$root/.goat-flow/hooks/deny-dangerous.sh\"'"
Comment thread .agents/hooks.json
{
"type": "command",
"command": "bash -c 'gcd=\"$(git rev-parse --git-common-dir 2>/dev/null)\"; root=\"\"; case \"$gcd\" in */.git/modules/*|.git/modules/*) root=\"$(git rev-parse --show-toplevel 2>/dev/null || true)\" ;; /*|[A-Za-z]:/*|[A-Za-z]:\\\\*) gcd=\"${gcd//\\\\//}\"; root=\"$(dirname \"$gcd\")\" ;; *) root=\"$(git rev-parse --show-toplevel 2>/dev/null || true)\" ;; esac; [ -f \"$root/.goat-flow/hooks/deny-dangerous.sh\" ] || root=\"${CLAUDE_PROJECT_DIR:-}\"; [ -f \"$root/.goat-flow/hooks/deny-dangerous.sh\" ] || { printf '\\''{\"decision\":\"deny\",\"reason\":\"Policy hook unavailable: git repository root unavailable.\"}\\n'\\''; exit 0; }; cd \"$root\" || { printf '\\''{\"decision\":\"deny\",\"reason\":\"Policy hook unavailable: git repository root unavailable.\"}\\n'\\''; exit 0; }; bash \"$root/.goat-flow/hooks/deny-dangerous.sh\"'",
"command": "bash -c 'root=\"$(git rev-parse --show-toplevel 2>/dev/null || true)\"; [ -f \"$root/.goat-flow/hooks/deny-dangerous.sh\" ] || root=\"${CLAUDE_PROJECT_DIR:-}\"; [ -f \"$root/.goat-flow/hooks/deny-dangerous.sh\" ] || { printf '\\''{\"decision\":\"deny\",\"reason\":\"Policy hook unavailable: git repository root unavailable.\"}\\n'\\''; exit 0; }; cd \"$root\" || { printf '\\''{\"decision\":\"deny\",\"reason\":\"Policy hook unavailable: git repository root unavailable.\"}\\n'\\''; exit 0; }; bash \"$root/.goat-flow/hooks/deny-dangerous.sh\"'",
Comment thread test/unit/hook-registrar.test.ts Outdated
Comment thread .goat-flow/hooks/gruff-code-quality.sh

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/cli/audit/harness/check-context.ts (1)

473-505: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't return PASS when checkAllDocPaths() already recorded a missing architecture doc.

collectArchitectureDocPaths() can add "architecture.md does not exist" to findings, but this branch still returns pass whenever the remaining path counts balance. A repo with valid router/core refs and no .goat-flow/architecture.md will therefore report doc-paths-resolve as green. Split hard failures from informational messages, or carry an explicit failure bit through the accumulator.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/audit/harness/check-context.ts` around lines 473 - 505,
checkAllDocPaths currently returns findings that may include hard failures (e.g.
missing architecture.md) but the caller in check-context.ts only uses counts to
decide PASS; update the contract so checkAllDocPaths returns an explicit failure
bit (e.g. hasCriticalFailure or hardFailure) alongside
totalPaths/resolvedCount/findings/unresolved, set that bit inside
collectArchitectureDocPaths when it records "architecture.md does not exist",
and then in the caller (the function in check-context.ts that currently branches
on totalPaths/resolvedCount) short-circuit to return fail(...) whenever
hasCriticalFailure is true (using the existing findings/details) so missing
architecture.md is treated as a hard failure even if counts balance.
.goat-flow/learning-loop/lessons/hook-testing.md (1)

112-121: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Align the lesson heading with the updated guidance.

Line 112 states a blanket “should not depend on shell substitution,” but Line 120 now documents an approved bash -c + git rev-parse shape. Rename the heading so it matches the new policy and avoids future misreads.

Suggested wording update
-## Lesson: Codex hook commands should not depend on shell substitution
+## Lesson: Codex hook commands must resolve repo root reliably across nested cwd
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.goat-flow/learning-loop/lessons/hook-testing.md around lines 112 - 121,
Rename the lesson heading "Lesson: Codex hook commands should not depend on
shell substitution" to reflect the narrowed policy that allows a specific safe
shape; update it to something like "Lesson: Codex hook commands must use a safe
git-root resolving wrapper (allow bash -c + git rev-parse)" so the heading
aligns with the body that documents the approved `bash -c` + `git rev-parse
--show-toplevel` wrapper and avoids implying a blanket ban on all shell
substitution.
🧹 Nitpick comments (3)
workflow/hooks/deny-dangerous/patterns-writes.sh (1)

271-296: 💤 Low value

Consolidate duplicated scan variable (mirror of .goat-flow/ script).

Lines 271 and 289 both derive an identically transformed value from CMD_TRIMMED. Consider using a single local scan="${CMD_TRIMMED//||/__GOAT_OR__}" variable for both the git and gh loops to reduce duplication. This mirrors the same optional refactor suggested for .goat-flow/hooks/deny-dangerous/patterns-writes.sh.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@workflow/hooks/deny-dangerous/patterns-writes.sh` around lines 271 - 296, The
code duplicates the same transformed value of CMD_TRIMMED into two variables
(repo_scan and gh_scan); replace both with a single local scan variable (e.g.,
local scan="${CMD_TRIMMED//||/__GOAT_OR__}") and reuse that variable in the git
loop (where normalize_git_policy_candidate, is_git_push, is_git_commit,
is_git_destructive and block are used) and in the GH loop (where
is_gh_write_operation and block are used) to eliminate duplication while
preserving existing logic and returns.
.goat-flow/hooks/deny-dangerous/patterns-writes.sh (1)

271-296: 💤 Low value

Consolidate duplicated scan variable.

Lines 271 and 289 both derive an identically transformed value from CMD_TRIMMED. Consider using a single local scan="${CMD_TRIMMED//||/__GOAT_OR__}" variable for both the git and gh loops to reduce duplication.

♻️ Proposed consolidation
-  local repo_scan="${CMD_TRIMMED//||/__GOAT_OR__}"
+  local scan="${CMD_TRIMMED//||/__GOAT_OR__}"
   local -a pipe_parts
   local pipe_part
-  IFS='|' read -ra pipe_parts <<< "$repo_scan"
+  IFS='|' read -ra pipe_parts <<< "$scan"
   for pipe_part in "${pipe_parts[@]}"; do
     local git_candidate
     git_candidate=$(normalize_git_policy_candidate "$pipe_part")
     if is_git_push "$git_candidate"; then
       block "git push is not allowed. Ask the user to push manually." || return $?
     fi
     if is_git_commit "$git_candidate"; then
       block "git commit is not allowed. Ask the user to commit manually." || return $?
     fi
     if is_git_destructive "$git_candidate"; then
       block "Destructive git operation (--no-verify / reset --hard / clean -f). Remove the flag, stash first, or run manually." || return $?
     fi
   done
 
-  local gh_scan="${CMD_TRIMMED//||/__GOAT_OR__}"
   local -a gh_pipe_parts
-  IFS='|' read -ra gh_pipe_parts <<< "$gh_scan"
+  IFS='|' read -ra gh_pipe_parts <<< "$scan"
   for pipe_part in "${gh_pipe_parts[@]}"; do
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.goat-flow/hooks/deny-dangerous/patterns-writes.sh around lines 271 - 296,
The code duplicates the same transformed CMD_TRIMMED into two variables
(repo_scan and gh_scan); consolidate by creating a single local variable (e.g.,
scan="${CMD_TRIMMED//||/__GOAT_OR__}") and reuse it in both loops, replacing
references to repo_scan and gh_scan with scan; ensure you still create separate
arrays (pipe_parts / gh_pipe_parts or reuse a single array by re-reading into it
before the second loop), preserve IFS='|' read -ra usage, and keep calls to
normalize_git_policy_candidate, is_git_push, is_git_commit, is_git_destructive,
is_gh_write_operation and block unchanged so behavior remains identical.
workflow/install-goat-flow.sh (1)

733-750: ⚡ Quick win

Reuse rootResolvingCommand() for the printed Claude recovery snippet.

The install path now centralizes resolver construction in rootResolvingCommand(), but Line 1796 still embeds a second copy of the same shell wrapper. The next resolver tweak can update the migrator while leaving the manual recovery guidance stale again.

Also applies to: 1794-1796

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@workflow/install-goat-flow.sh` around lines 733 - 750, The Claude recovery
snippet still duplicates the shell wrapper; replace the inlined shell snippet
used for printed/manual recovery with a call to rootResolvingCommand(script) so
the same resolver logic (including the claudeRootFallback and
hookUnavailableCommand behavior) is reused; locate the print/migrator code that
currently inlines the bash wrapper for the same hook script and substitute it to
use the rootResolvingCommand() return value (ensure you pass the same script
identifier used there and preserve any surrounding quoting/escaping for
embedding in printed output).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.goat-flow/hooks/deny-dangerous.sh:
- Around line 85-89: The repository's mirrored deny-dangerous hooks contain
several functions (e.g., find_matching_shell_paren, check_command_substitutions,
__goat_git_strip_globals) that ShellCheck reports as "never invoked" (SC2329);
to fix, either remove truly unused helper functions from
.goat-flow/hooks/deny-dangerous.sh, or if they are intentionally kept for
future/externally-invoked use, annotate each such function with an inline
ShellCheck suppression comment (e.g., # shellcheck disable=SC2329) or add a
small internal dispatcher that calls them so they are recognized as used; update
the file so ShellCheck exits zero while keeping the intended behavior and ensure
you reference the exact function names above when making edits.

In @.goat-flow/hooks/gruff-code-quality.sh:
- Around line 292-304: In payload_supported_file_paths, after normalizing
slashes, detect Windows drive-letter absolute paths (regex like ^[A-Za-z]:/) and
treat them as absolute instead of falling through to the repo-relative branch:
either reject them early (skip/continue) or strip/normalize them into a form
comparable to normalized_root before calling relative_path; update the logic
that currently falls into the *) branch so relative_path() is never called for
drive-letter paths and mirror the same change in
workflow/hooks/gruff-code-quality.sh and any callers such as process_file.

In `@src/cli/facts/fs.ts`:
- Around line 197-204: createPathResolver currently calls resolve(root,
relativePath) but allows absolute inputs and ../ traversal to escape the project
root; change resolveProjectPath to first canonicalize the root (rootResolved =
resolve(root)), then compute resolved = resolve(rootResolved, relativePath) and
verify the resolved path is still inside root by using
path.relative(rootResolved, resolved) (reject when it starts with '..' or equals
'.' escape) — if the path escapes, return a sentinel that makes callers treat
the file as absent (e.g., empty string or null) instead of returning a path
outside the root; update callers of ResolvePath (ctx.fs.exists, ctx.fs.read,
ctx.fs.list) to treat that sentinel as “not found” so escaped or absolute inputs
are rejected rather than followed.

In `@src/dashboard/dashboard-app-data-loading-fragments.ts`:
- Around line 206-209: The catch currently always toasts errors, which can
surface stale failures after the user switches workspaces; capture the request's
identifying state (e.g., projectPath, runner, generation) when the inventory
load starts and, in the catch block, compare those captured values to the
current context before calling ctx.showToast(err...). Only call ctx.showToast if
the captured projectPath/runner/generation still match the current context (or
use a per-request token/ID that you compare), so late failures for a previous
request are ignored.

In `@src/dashboard/dashboard-app-project-terminal-fragments.ts`:
- Around line 50-78: In dashboardRunSkillEvaluator capture the current
ctx.projectPath (e.g., const requestedPath = ctx.projectPath) before making the
fetch, and after any awaited operations (after
res.json()/readRecord/readErrorMessage or in the catch) verify that
ctx.projectPath === requestedPath before assigning to ctx.skillEvaluatorResult,
ctx.skillEvaluatorError, or ctx.skillEvaluatorLoading; if they differ, abort
applying the response (do not overwrite state) so results from a previous
project don't leak into the current evaluator pane. Ensure this check is applied
in both the success branch and the catch/finally handling.

In `@test/unit/dashboard-terminal-launch/fakes.ts`:
- Around line 80-82: The fake-clock requeues intervals at state.now +
timer.intervalMs which, when timer.intervalMs is 0 (because callers pass ms ??
0), re-schedules them at the same timestamp and lets tick() spin forever; change
the reschedule logic in the state.timers.set call to enforce a minimum positive
delay (e.g., nextAt = state.now + Math.max(1, timer.intervalMs)) and also ensure
the place that passes ms ?? 0 uses a minimum of 1 for interval scheduling so
zero-delay intervals become 1ms instead of 0ms.

In `@test/unit/hook-registrar.test.ts`:
- Around line 270-279: The current assertions using
claudeGruffCommands.every(...) can vacuously pass when claudeGruffCommands is
empty; add an explicit non-empty check before those .every checks (e.g.,
assert.ok(claudeGruffCommands.length > 0) or
assert.notStrictEqual(claudeGruffCommands.length, 0)) so the test fails if no
gruff commands were produced, then keep the existing asserts that each command
includes "gruff-code-quality: hook unavailable" and that none include "BLOCKED:
Policy hook unavailable".

In `@workflow/hooks/agent-config/claude.json`:
- Line 62: The current inline shell falls back to CLAUDE_PROJECT_DIR whenever
the hook file is missing, which can run a policy from another project; modify
the shell logic so you first attempt git rev-parse to set root, only if that
fails set root="${CLAUDE_PROJECT_DIR:-}", and after selecting the root strictly
check for the presence of "$root/.goat-flow/hooks/deny-dangerous.sh" (do not
re-run the fallback when the hook is absent). Update the command string that
references git rev-parse, CLAUDE_PROJECT_DIR, and deny-dangerous.sh to:
determine root via git rev-parse || use CLAUDE_PROJECT_DIR, then if the hook
file is missing print the BLOCKED error and exit (no further fallback), cd to
$root and execute .goat-flow/hooks/deny-dangerous.sh.

---

Outside diff comments:
In @.goat-flow/learning-loop/lessons/hook-testing.md:
- Around line 112-121: Rename the lesson heading "Lesson: Codex hook commands
should not depend on shell substitution" to reflect the narrowed policy that
allows a specific safe shape; update it to something like "Lesson: Codex hook
commands must use a safe git-root resolving wrapper (allow bash -c + git
rev-parse)" so the heading aligns with the body that documents the approved
`bash -c` + `git rev-parse --show-toplevel` wrapper and avoids implying a
blanket ban on all shell substitution.

In `@src/cli/audit/harness/check-context.ts`:
- Around line 473-505: checkAllDocPaths currently returns findings that may
include hard failures (e.g. missing architecture.md) but the caller in
check-context.ts only uses counts to decide PASS; update the contract so
checkAllDocPaths returns an explicit failure bit (e.g. hasCriticalFailure or
hardFailure) alongside totalPaths/resolvedCount/findings/unresolved, set that
bit inside collectArchitectureDocPaths when it records "architecture.md does not
exist", and then in the caller (the function in check-context.ts that currently
branches on totalPaths/resolvedCount) short-circuit to return fail(...) whenever
hasCriticalFailure is true (using the existing findings/details) so missing
architecture.md is treated as a hard failure even if counts balance.

---

Nitpick comments:
In @.goat-flow/hooks/deny-dangerous/patterns-writes.sh:
- Around line 271-296: The code duplicates the same transformed CMD_TRIMMED into
two variables (repo_scan and gh_scan); consolidate by creating a single local
variable (e.g., scan="${CMD_TRIMMED//||/__GOAT_OR__}") and reuse it in both
loops, replacing references to repo_scan and gh_scan with scan; ensure you still
create separate arrays (pipe_parts / gh_pipe_parts or reuse a single array by
re-reading into it before the second loop), preserve IFS='|' read -ra usage, and
keep calls to normalize_git_policy_candidate, is_git_push, is_git_commit,
is_git_destructive, is_gh_write_operation and block unchanged so behavior
remains identical.

In `@workflow/hooks/deny-dangerous/patterns-writes.sh`:
- Around line 271-296: The code duplicates the same transformed value of
CMD_TRIMMED into two variables (repo_scan and gh_scan); replace both with a
single local scan variable (e.g., local scan="${CMD_TRIMMED//||/__GOAT_OR__}")
and reuse that variable in the git loop (where normalize_git_policy_candidate,
is_git_push, is_git_commit, is_git_destructive and block are used) and in the GH
loop (where is_gh_write_operation and block are used) to eliminate duplication
while preserving existing logic and returns.

In `@workflow/install-goat-flow.sh`:
- Around line 733-750: The Claude recovery snippet still duplicates the shell
wrapper; replace the inlined shell snippet used for printed/manual recovery with
a call to rootResolvingCommand(script) so the same resolver logic (including the
claudeRootFallback and hookUnavailableCommand behavior) is reused; locate the
print/migrator code that currently inlines the bash wrapper for the same hook
script and substitute it to use the rootResolvingCommand() return value (ensure
you pass the same script identifier used there and preserve any surrounding
quoting/escaping for embedding in printed output).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c1f7520c-7110-4015-adc0-cab792ff1bb6

📥 Commits

Reviewing files that changed from the base of the PR and between 886a527 and 706c71a.

📒 Files selected for processing (78)
  • .agents/hooks.json
  • .agents/skills/goat-critique/SKILL.md
  • .claude/settings.json
  • .claude/skills/goat-critique/SKILL.md
  • .codex/hooks.json
  • .github/skills/goat-critique/SKILL.md
  • .goat-flow/hooks/deny-dangerous.sh
  • .goat-flow/hooks/deny-dangerous/deny-dangerous-self-test.sh
  • .goat-flow/hooks/deny-dangerous/patterns-shell.sh
  • .goat-flow/hooks/deny-dangerous/patterns-writes.sh
  • .goat-flow/hooks/gruff-code-quality.sh
  • .goat-flow/learning-loop/decisions/ADR-032-scope-gruff-hook-binary-discovery.md
  • .goat-flow/learning-loop/footguns/auditor.md
  • .goat-flow/learning-loop/footguns/deny-dangerous.md
  • .goat-flow/learning-loop/footguns/hooks.md
  • .goat-flow/learning-loop/lessons/dashboard-testing.md
  • .goat-flow/learning-loop/lessons/gruff-cleanup.md
  • .goat-flow/learning-loop/lessons/hook-testing.md
  • .goat-flow/learning-loop/lessons/verification.md
  • .gruff-ts.yaml
  • CHANGELOG.md
  • scripts/check-instruction-parity.mjs
  • scripts/preflight-checks.sh
  • scripts/profile-dashboard-audit.mjs
  • src/cli/audit/check-agent-deny-mechanism.ts
  • src/cli/audit/harness/check-context.ts
  • src/cli/facts/fs.ts
  • src/cli/server/agent-hook-writer.ts
  • src/cli/server/dashboard-audit-routes.ts
  • src/cli/server/safe-exec.ts
  • src/dashboard/app.ts
  • src/dashboard/dashboard-app-data-loading-fragments.ts
  • src/dashboard/dashboard-app-init.ts
  • src/dashboard/dashboard-app-project-terminal-fragments.ts
  • src/dashboard/dashboard-app-prompts-audit-fragments.ts
  • src/dashboard/dashboard-app-skill-quality-fragments.ts
  • src/dashboard/dashboard-app-state-fragments.ts
  • src/dashboard/dashboard-model-readers.ts
  • src/dashboard/dashboard-prompts.ts
  • src/dashboard/dashboard-readers.ts
  • test/fixtures/projects/index.ts
  • test/integration/audit-drift-checkdrift-hook-templates.test.ts
  • test/integration/gruff-code-quality-smoke.test.ts
  • test/integration/path-integrity.test.ts
  • test/integration/setup-install-codex-config-migration.test.ts
  • test/integration/setup-install.test.ts
  • test/integration/skill-author.test.ts
  • test/smoke/dashboard-endpoints.test.ts
  • test/src.ts
  • test/unit/agent-config-template-parity.test.ts
  • test/unit/audit-command/agent-deny-hooks.test.ts
  • test/unit/audit-command/helpers.ts
  • test/unit/audit-command/hook-facts.test.ts
  • test/unit/audit-command/main.test.ts
  • test/unit/audit-harness/check-evidence-before-claims.test.ts
  • test/unit/check-content-quality.test.ts
  • test/unit/config-writer.test.ts
  • test/unit/dashboard-home.test.ts
  • test/unit/dashboard-readers.test.ts
  • test/unit/dashboard-reporting.test.ts
  • test/unit/dashboard-terminal-launch/fakes.ts
  • test/unit/dashboard-terminal-launch/launch-flow-06.test.ts
  • test/unit/hook-registrar.test.ts
  • test/unit/local-paths.test.ts
  • test/unit/skill-quality/helpers.ts
  • test/unit/skill-quality/main.test.ts
  • test/unit/terminal-uploads.test.ts
  • workflow/hooks/README.md
  • workflow/hooks/agent-config/antigravity-hooks.json
  • workflow/hooks/agent-config/claude.json
  • workflow/hooks/agent-config/codex-hooks.json
  • workflow/hooks/deny-dangerous.sh
  • workflow/hooks/deny-dangerous/deny-dangerous-self-test.sh
  • workflow/hooks/deny-dangerous/patterns-shell.sh
  • workflow/hooks/deny-dangerous/patterns-writes.sh
  • workflow/hooks/gruff-code-quality.sh
  • workflow/install-goat-flow.sh
  • workflow/skills/goat-critique/SKILL.md

Comment on lines +85 to 89
local top_level
top_level="$(git rev-parse --show-toplevel 2>/dev/null)" || return 1
[[ -n "$top_level" ]] || return 1
printf '%s\n' "$top_level"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
shellcheck .goat-flow/hooks/deny-dangerous.sh workflow/hooks/deny-dangerous.sh

Repository: blundergoat/goat-flow

Length of output: 2332


Run ShellCheck on mirrored deny-dangerous hooks (and note current findings)

ShellCheck was run on both .goat-flow/hooks/deny-dangerous.sh and workflow/hooks/deny-dangerous.sh, but it exited non-zero due to SC2329 “function is never invoked” (e.g., find_matching_shell_paren, check_command_substitutions, __goat_git_strip_globals, etc.).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.goat-flow/hooks/deny-dangerous.sh around lines 85 - 89, The repository's
mirrored deny-dangerous hooks contain several functions (e.g.,
find_matching_shell_paren, check_command_substitutions,
__goat_git_strip_globals) that ShellCheck reports as "never invoked" (SC2329);
to fix, either remove truly unused helper functions from
.goat-flow/hooks/deny-dangerous.sh, or if they are intentionally kept for
future/externally-invoked use, annotate each such function with an inline
ShellCheck suppression comment (e.g., # shellcheck disable=SC2329) or add a
small internal dispatcher that calls them so they are recognized as used; update
the file so ShellCheck exits zero while keeping the intended behavior and ensure
you reference the exact function names above when making edits.

Source: Coding guidelines

Comment thread .goat-flow/hooks/gruff-code-quality.sh
Comment thread src/cli/facts/fs.ts
Comment on lines +197 to +204
/** Build the resolver that anchors all fact reads under the selected project root. */
function createPathResolver(root: string): ResolvePath {
/** Resolve one caller-supplied relative path under the adapter root. */
function resolveProjectPath(relativePath: string): string {
return resolve(root, relativePath);
}
return resolveProjectPath;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

createPathResolver() does not actually confine reads to the audited root.

resolve(root, relativePath) still accepts absolute inputs and ../ traversal. In this PR, check-context.ts feeds project-authored doc/router refs into ctx.fs.exists(...), so a malicious workspace can probe files outside the selected project instead of being sandboxed to it. Please reject escaped paths and keep the read/exists/list helpers treating them as absent rather than following them.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/facts/fs.ts` around lines 197 - 204, createPathResolver currently
calls resolve(root, relativePath) but allows absolute inputs and ../ traversal
to escape the project root; change resolveProjectPath to first canonicalize the
root (rootResolved = resolve(root)), then compute resolved =
resolve(rootResolved, relativePath) and verify the resolved path is still inside
root by using path.relative(rootResolved, resolved) (reject when it starts with
'..' or equals '.' escape) — if the path escapes, return a sentinel that makes
callers treat the file as absent (e.g., empty string or null) instead of
returning a path outside the root; update callers of ResolvePath (ctx.fs.exists,
ctx.fs.read, ctx.fs.list) to treat that sentinel as “not found” so escaped or
absolute inputs are rejected rather than followed.

Comment thread src/dashboard/dashboard-app-data-loading-fragments.ts
Comment thread src/dashboard/dashboard-app-project-terminal-fragments.ts
Comment thread test/unit/dashboard-terminal-launch/fakes.ts
Comment thread test/unit/hook-registrar.test.ts
{
"type": "command",
"command": "bash -c 'gcd=\"$(git rev-parse --git-common-dir 2>/dev/null)\"; root=\"\"; case \"$gcd\" in */.git/modules/*|.git/modules/*) root=\"$(git rev-parse --show-toplevel 2>/dev/null || true)\" ;; /*|[A-Za-z]:/*|[A-Za-z]:\\\\*) gcd=\"${gcd//\\\\//}\"; root=\"$(dirname \"$gcd\")\" ;; *) root=\"$(git rev-parse --show-toplevel 2>/dev/null || true)\" ;; esac; [ -f \"$root/.goat-flow/hooks/deny-dangerous.sh\" ] || root=\"${CLAUDE_PROJECT_DIR:-}\"; [ -f \"$root/.goat-flow/hooks/deny-dangerous.sh\" ] || { printf '\\''BLOCKED: Policy hook unavailable: git repository root unavailable.\\n'\\'' >&2; exit 2; }; cd \"$root\" || { printf '\\''BLOCKED: Policy hook unavailable: git repository root unavailable.\\n'\\'' >&2; exit 2; }; bash \"$root/.goat-flow/hooks/deny-dangerous.sh\"'"
"command": "bash -c 'root=\"$(git rev-parse --show-toplevel 2>/dev/null || true)\"; [ -f \"$root/.goat-flow/hooks/deny-dangerous.sh\" ] || root=\"${CLAUDE_PROJECT_DIR:-}\"; [ -f \"$root/.goat-flow/hooks/deny-dangerous.sh\" ] || { printf '\\''BLOCKED: Policy hook unavailable: git repository root unavailable.\\n'\\'' >&2; exit 2; }; cd \"$root\" || { printf '\\''BLOCKED: Policy hook unavailable: git repository root unavailable.\\n'\\'' >&2; exit 2; }; bash \"$root/.goat-flow/hooks/deny-dangerous.sh\"'"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fallback currently triggers on missing hook, not only missing git root.

Line 62 falls back to CLAUDE_PROJECT_DIR whenever $root/.goat-flow/hooks/deny-dangerous.sh is absent. That can execute policy from a different project root instead of failing closed for the active checkout.

Suggested fix
- "command": "bash -c 'root=\"$(git rev-parse --show-toplevel 2>/dev/null || true)\"; [ -f \"$root/.goat-flow/hooks/deny-dangerous.sh\" ] || root=\"${CLAUDE_PROJECT_DIR:-}\"; [ -f \"$root/.goat-flow/hooks/deny-dangerous.sh\" ] || { printf '\\''BLOCKED: Policy hook unavailable: git repository root unavailable.\\n'\\'' >&2; exit 2; }; cd \"$root\" || { printf '\\''BLOCKED: Policy hook unavailable: git repository root unavailable.\\n'\\'' >&2; exit 2; }; bash \"$root/.goat-flow/hooks/deny-dangerous.sh\"'"
+ "command": "bash -c 'root=\"$(git rev-parse --show-toplevel 2>/dev/null || true)\"; if [ -z \"$root\" ]; then root=\"${CLAUDE_PROJECT_DIR:-}\"; fi; [ -f \"$root/.goat-flow/hooks/deny-dangerous.sh\" ] || { printf '\\''BLOCKED: Policy hook unavailable: git repository root unavailable.\\n'\\'' >&2; exit 2; }; cd \"$root\" || { printf '\\''BLOCKED: Policy hook unavailable: git repository root unavailable.\\n'\\'' >&2; exit 2; }; bash \"$root/.goat-flow/hooks/deny-dangerous.sh\"'"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"command": "bash -c 'root=\"$(git rev-parse --show-toplevel 2>/dev/null || true)\"; [ -f \"$root/.goat-flow/hooks/deny-dangerous.sh\" ] || root=\"${CLAUDE_PROJECT_DIR:-}\"; [ -f \"$root/.goat-flow/hooks/deny-dangerous.sh\" ] || { printf '\\''BLOCKED: Policy hook unavailable: git repository root unavailable.\\n'\\'' >&2; exit 2; }; cd \"$root\" || { printf '\\''BLOCKED: Policy hook unavailable: git repository root unavailable.\\n'\\'' >&2; exit 2; }; bash \"$root/.goat-flow/hooks/deny-dangerous.sh\"'"
"command": "bash -c 'root=\"$(git rev-parse --show-toplevel 2>/dev/null || true)\"; if [ -z \"$root\" ]; then root=\"${CLAUDE_PROJECT_DIR:-}\"; fi; [ -f \"$root/.goat-flow/hooks/deny-dangerous.sh\" ] || { printf '\\''BLOCKED: Policy hook unavailable: git repository root unavailable.\\n'\\'' >&2; exit 2; }; cd \"$root\" || { printf '\\''BLOCKED: Policy hook unavailable: git repository root unavailable.\\n'\\'' >&2; exit 2; }; bash \"$root/.goat-flow/hooks/deny-dangerous.sh\"'"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@workflow/hooks/agent-config/claude.json` at line 62, The current inline shell
falls back to CLAUDE_PROJECT_DIR whenever the hook file is missing, which can
run a policy from another project; modify the shell logic so you first attempt
git rev-parse to set root, only if that fails set
root="${CLAUDE_PROJECT_DIR:-}", and after selecting the root strictly check for
the presence of "$root/.goat-flow/hooks/deny-dangerous.sh" (do not re-run the
fallback when the hook is absent). Update the command string that references git
rev-parse, CLAUDE_PROJECT_DIR, and deny-dangerous.sh to: determine root via git
rev-parse || use CLAUDE_PROJECT_DIR, then if the hook file is missing print the
BLOCKED error and exit (no further fallback), cd to $root and execute
.goat-flow/hooks/deny-dangerous.sh.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0e53df2e99

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

SKIP_DIR_PATTERN='(^|/)(node_modules|vendor|\.goat-flow|dist|build|coverage|\.git|target|\.venv|\.mypy_cache|\.pytest_cache|\.ruff_cache)(/|$)'
GRUFF_CODE_QUALITY_TIMEOUT_SECONDS="${GRUFF_CODE_QUALITY_TIMEOUT_SECONDS:-30}"
BINARY_SEARCH_PATHS='vendor/bin, node_modules/.bin, bin, .venv/bin, ~/.local/bin, PATH'
GRUFF_CODE_QUALITY_TIMEOUT_SECONDS="${GRUFF_CODE_QUALITY_TIMEOUT_SECONDS:-60}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep gruff timeout below runner timeout

When this hook is installed through the .agents/Antigravity config, the hook entry still has "timeout": 30 while the analyzer now defaults to 60 seconds. For analyzers that need 31–60 seconds, the runner kills the wrapper before run_gruff_json can reach its own timeout path and print the actionable diagnostic, so the new timeout is ineffective and the edit gets an abrupt hook timeout instead. Either keep the default within the registered hook timeout or bump the generated hook timeout in the installed/template configs at the same time.

Useful? React with 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 244 out of 246 changed files in this pull request and generated 3 comments.

Comment on lines 98 to 101
/** Cast this focused fake to the WebSocket subset TerminalManager consumes. */
asTerminalSocket(): TerminalWebSocket {
return this as unknown as TerminalWebSocket;
return this as TerminalWebSocket;
}
Comment on lines +44 to +48
const entries = parseBucket(fs, dirPath, bucket);
writeFileSync(
join(projectPath, indexRelPath),
formatIndex(bucket, entries),
);
Comment thread package.json
Comment on lines 1 to 4
{
"name": "@blundergoat/goat-flow",
"version": "1.10.1",
"version": "1.11.0",
"description": "AI coding agent harness and local dashboard for Claude Code, OpenAI Codex, Google Antigravity, and GitHub Copilot - setup audits, guardrails, structured skills, deny hooks, and persistent learning loops.",

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3fc944aba8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

c=$(normalize_command_candidate "$1")

local xargs_rest=""
if xargs_rest=$(strip_xargs_prefix "$c"); then

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle xargs arg-file before stripping git commands

When a repository write is wrapped with xargs -a/--arg-file, this new normalization trusts strip_xargs_prefix, but that parser treats -a as a valueless option; GNU xargs --help shows -a, --arg-file=FILE consumes the next operand. As a result xargs -a msgs git commit -F - normalizes to msgs git commit -F -, so the newly added is_git_commit/destructive checks do not see git and the deny hook exits 0, letting the exact xargs-wrapped git writes this change is trying to block slip through.

Useful? React with 👍 / 👎.

@mattyhansen mattyhansen changed the title v0.4.0 v1.11.0 Jun 10, 2026
Comment thread .goat-flow/hooks/gruff-code-quality.sh

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 85599188f6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

local pipe_index
IFS='|' read -ra pipeline_parts <<< "$pipe_scan"
for ((pipe_index = 0; pipe_index < ${#pipeline_parts[@]}; pipe_index++)); do
check_xargs_destructive_payload "${pipeline_parts[$pipe_index]}" || return $?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Parse xargs arg files before checking payloads

When the new per-pipeline xargs check handles a segment such as printf x | xargs -a targets rm -rf, it delegates to strip_xargs_payload_command, but that parser still treats -a as a flag without an operand; xargs --help documents -a, --arg-file=FILE as reading arguments from a file. The normalized payload becomes targets rm -rf, so rm_has_recursive never sees rm, and I reproduced the hook exiting 0 for that command while blocking the same shape without -a. This leaves an easy bypass for the destructive xargs pipeline this change is trying to catch.

Useful? React with 👍 / 👎.

Comment thread .goat-flow/hooks/deny-dangerous/patterns-writes.sh

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 1d2bdc1. Configure here.

Comment thread .codex/hooks.json
{
"type": "command",
"command": ".goat-flow/hooks/gruff-code-quality.sh",
"command": "bash -c 'root=\"$(git rev-parse --show-toplevel 2>/dev/null || true)\"; [ -f \"$root/.goat-flow/hooks/gruff-code-quality.sh\" ] || { printf '\\''gruff-code-quality: hook unavailable: git repository root or hook script unavailable; skipped.\\n'\\'' >&2; exit 0; }; cd \"$root\" || { printf '\\''gruff-code-quality: hook unavailable: git repository root or hook script unavailable; skipped.\\n'\\'' >&2; exit 0; }; bash \"$root/.goat-flow/hooks/gruff-code-quality.sh\"'",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Codex gruff hook lacks timeout

Low Severity

PostToolUse gruff-code-quality hooks were raised to a 90s timeout in .agents/hooks.json, .claude/settings.json, and .github/hooks/hooks.json, but .codex/hooks.json still omits a timeout on the same wrapper commands. Codex may kill the hook earlier than other runtimes during slower analyzer runs.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1d2bdc1. Configure here.

@mattyhansen mattyhansen merged commit 8a6dd5f into main Jun 10, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants