[failproofai-483] Add hook-sync container for daily agent-CLI drift detection#483
Conversation
A single-shot container (docker/) that clones failproofai and runs Claude Code in --effort ultracode mode to detect hook event-name, tool/payload-schema, and settings-file-shape drift across all seven integrated agent CLIs, then opens one auto-sync PR. The agent commits, pushes, and opens the PR itself, gated by failproofai's own require-*-before-stop hooks (dogfooding). Published to ghcr.io/failproofai/hook-sync via .github/workflows/build-image.yml. Replaces the broken sync-hook-events.yml GitHub Action (removed along with its prompt). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gYNtmxTPnTyeQeUmEHA28
|
Warning Review limit reached
Next review available in: 11 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
📝 WalkthroughWalkthroughAdds a new ChangesHook-sync automation replacement
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Scheduler
participant Container
participant GitHub
participant Claude
participant Webhook
Scheduler->>Container: daily cron trigger
Container->>Container: validate env, auth setup
Container->>GitHub: clone repo, create branch
Container->>Container: edit policies-config.json
Container->>Claude: run ultracode sync via PTY
Claude-->>Container: stream output + exit code
Container->>GitHub: open auto-sync PR (if drift detected)
Container->>Webhook: post start/success/failure telemetry
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| - name: Log in to GHCR | ||
| uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 | ||
| with: | ||
| registry: ghcr.io | ||
| username: ${{ github.actor }} | ||
| password: ${{ secrets.GITHUB_TOKEN }} |
There was a problem hiding this comment.
why do we need this? we can login with the repo itself?
There was a problem hiding this comment.
This step is logging in with the repo itself — username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }}, no external credential. It has to stay because docker/build-push-action pushes through the Docker daemon, which must be authenticated to ghcr.io first; permissions: packages: write only grants the token the scope, it doesn't log the daemon in. Without this step the push fails with a 401.
| - name: Build and push | ||
| uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 | ||
| with: | ||
| context: docker | ||
| file: docker/Dockerfile | ||
| push: ${{ github.event_name != 'workflow_dispatch' || inputs.push_to_ghcr }} | ||
| tags: ${{ steps.tags.outputs.tags }} | ||
| cache-from: type=gha | ||
| cache-to: type=gha,mode=max | ||
| provenance: false |
There was a problem hiding this comment.
:latest is already tagged — the "Compute tags" step emits ghcr.io/failproofai/hook-sync:latest unconditionally (alongside :sha-<short>), so every push/schedule build publishes it. No change needed.
| notify() { # <ignored-level> <text> | ||
| [ -z "$HOOK_SYNC_WEBHOOK_URL" ] && return 0 | ||
| HOOK_SYNC_TEXT="$2" node -e ' | ||
| const { URL } = require("url"); | ||
| const u = new URL(process.env.HOOK_SYNC_WEBHOOK_URL); | ||
| const lib = u.protocol === "http:" ? require("http") : require("https"); | ||
| const body = JSON.stringify({ text: process.env.HOOK_SYNC_TEXT }); | ||
| const req = lib.request(u, { method: "POST", headers: { "content-type": "application/json", "content-length": Buffer.byteLength(body) } }, (res) => res.resume()); | ||
| req.on("error", () => {}); | ||
| req.write(body); | ||
| req.end(); | ||
| ' 2>/dev/null || log "WARN: webhook post failed" | ||
| } |
There was a problem hiding this comment.
we do not need for now.. lets remove... no dead code.
There was a problem hiding this comment.
Done in 37b443b — removed the webhook telemetry entirely (the notify() function + HOOK_SYNC_WEBHOOK_URL) and the now-unused /var/log/hook-sync dir. Run output already streams to the pod log via stdout, so no separate telemetry channel is needed.
…open PR Review feedback on #483: - Remove the optional webhook telemetry (notify() / HOOK_SYNC_WEBHOOK_URL) from the entrypoint and the now-unused /var/log/hook-sync dir from the image — speculative, no dead code. Run output already streams to the pod log via stdout. - Reshape the ultracode prompt so the sync agent checks for an already-open sync PR BEFORE editing anything: if one is open it does not open a second PR — it comments the missing drift on that PR (or does nothing if fully covered). Moving the dedupe ahead of edits keeps the working tree clean on the comment / no-op paths, which the require-*-before-stop hooks allow (they only enforce when the branch has commits ahead of origin/main). The login-to-GHCR step and the :latest tag stay: the login uses the repo's own GITHUB_TOKEN and is required for build-push-action to push; :latest is already tagged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gYNtmxTPnTyeQeUmEHA28
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
.github/workflows/build-image.yml (1)
37-87: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winAdd a
concurrencygroup to prevent simultaneous builds.A push to
docker/**around 08:00 UTC can overlap with the cron rebuild. Both builds would race to pushghcr.io/failproofai/hook-sync:latest, and the last writer wins — potentially tagging an older SHA as:latest. A concurrency group also cancels superseded runs and saves CI minutes.♻️ Suggested addition (place between `permissions` and `jobs`)
permissions: contents: read packages: write + +concurrency: + group: build-image-${{ github.ref }} + cancel-in-progress: true jobs:🤖 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 @.github/workflows/build-image.yml around lines 37 - 87, Add a workflow-level concurrency group to the build-image workflow so only one build runs for the same ref at a time and newer runs cancel older ones. Update the top-level workflow config (between permissions and jobs) to use a stable group based on the workflow and ref, and enable cancel-in-progress so overlapping push and cron runs cannot race in the Build and push step or overwrite ghcr.io/failproofai/hook-sync:latest.docker/entrypoint.sh (1)
115-115: 🔒 Security & Privacy | 🔵 Trivial
--dangerously-skip-permissionsremoves the last execution guardrail.This is necessary for headless autonomous operation, but it means the prompt-file constraints (lines 189-205) are advisory, not enforced — a confused agent could run arbitrary commands. The failproofai Stop hooks (require-commit/push/pr-before-stop) still gate the end of the run, but not intermediate actions. The branch isolation and ephemeral container limit the blast radius, which is the right mitigation. Just confirming this is an intentional design tradeoff, not an oversight.
Also applies to: 115-115
🤖 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 `@docker/entrypoint.sh` at line 115, The use of `--dangerously-skip-permissions` in `entrypoint.sh` is an intentional tradeoff for autonomous headless runs, so no code change is needed here; just verify that the prompt-file constraints remain documented as advisory only and that the existing failproofai stop hooks (`require-commit`, `require-push`, `pr-before-stop`) are treated as the only enforced end-of-run guards.
🤖 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 @.github/workflows/build-image.yml:
- Around line 3-7: The changelog entry is missing the required pull request
reference, so update the top `CHANGELOG.md` entry to include the `#NNN`
identifier while keeping the existing version text unchanged to match
`package.json`. Locate the affected release note section in `CHANGELOG.md` and
add the PR number in the entry text in the same style as other changelog items.
In `@docker/entrypoint.sh`:
- Around line 47-56: The webhook POST in the entrypoint’s Node snippet has no
timeout, so a slow or unreachable HOOK_SYNC_WEBHOOK_URL can block container
shutdown. Update the request logic inside the notify flow to set a short timeout
(for example 5s) on lib.request, and ensure the request is destroyed/aborted
when the timeout fires so the process can continue to exit. Keep the existing
error handling and logging path in place so notify still falls back to log WARN:
webhook post failed.
- Around line 72-74: The WORKSPACE guard in entrypoint.sh is too narrow for the
destructive cleanup later in the script. Extend the case block that validates
WORKSPACE to also reject $HOME and common system directories such as /home,
/etc, and /usr before the rm -rf path in the hook-sync flow. Keep the existing
unsafe-path check structure, but broaden the blocklist in the same WORKSPACE
validation logic so unsafe values are caught before git config, gh auth, and the
cleanup step run.
---
Nitpick comments:
In @.github/workflows/build-image.yml:
- Around line 37-87: Add a workflow-level concurrency group to the build-image
workflow so only one build runs for the same ref at a time and newer runs cancel
older ones. Update the top-level workflow config (between permissions and jobs)
to use a stable group based on the workflow and ref, and enable
cancel-in-progress so overlapping push and cron runs cannot race in the Build
and push step or overwrite ghcr.io/failproofai/hook-sync:latest.
In `@docker/entrypoint.sh`:
- Line 115: The use of `--dangerously-skip-permissions` in `entrypoint.sh` is an
intentional tradeoff for autonomous headless runs, so no code change is needed
here; just verify that the prompt-file constraints remain documented as advisory
only and that the existing failproofai stop hooks (`require-commit`,
`require-push`, `pr-before-stop`) are treated as the only enforced end-of-run
guards.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: b4e71a66-89ce-435d-9ef3-47614acc682b
📒 Files selected for processing (8)
.github/workflows/build-image.yml.github/workflows/sync-hook-events.yml.gitignoreCHANGELOG.mddocker/Dockerfiledocker/entrypoint.shscripts/sync-agent-cli-harnesses-prompt.mdscripts/sync-hook-events-prompt.md
💤 Files with no reviewable changes (3)
- scripts/sync-hook-events-prompt.md
- .gitignore
- .github/workflows/sync-hook-events.yml
| # Builds and pushes the failproofai hook-sync container to GHCR. | ||
| # | ||
| # The hook-sync container (docker/) runs Claude Code in --effort ultracode mode on a | ||
| # daily schedule (a k8s CronJob managed in our infra, not this repo) to detect drift | ||
| # between every integrated agent CLI and this repo, and opens auto-sync PRs. See |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify CHANGELOG compliance: PR number presence and version match with package.json.
echo "=== CHANGELOG entry (lines 3-7) ==="
sed -n '3,7p' CHANGELOG.md
echo ""
echo "=== PR number check ==="
if sed -n '3,7p' CHANGELOG.md | grep -qE '#[0-9]+'; then
echo "OK: PR number found"
else
echo "MISSING: No PR number (e.g., `#NNN`) in CHANGELOG entry"
fi
echo ""
echo "=== package.json version ==="
if [ -f package.json ]; then
jq -r '.version' package.json
else
echo "package.json not found at repo root"
fiRepository: FailproofAI/failproofai
Length of output: 980
Add the PR number to this changelog entry
CHANGELOG.md:3-7 is missing the required #NNN reference; the version already matches package.json.
🤖 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 @.github/workflows/build-image.yml around lines 3 - 7, The changelog entry is
missing the required pull request reference, so update the top `CHANGELOG.md`
entry to include the `#NNN` identifier while keeping the existing version text
unchanged to match `package.json`. Locate the affected release note section in
`CHANGELOG.md` and add the PR number in the entry text in the same style as
other changelog items.
Source: Coding guidelines
| HOOK_SYNC_TEXT="$2" node -e ' | ||
| const { URL } = require("url"); | ||
| const u = new URL(process.env.HOOK_SYNC_WEBHOOK_URL); | ||
| const lib = u.protocol === "http:" ? require("http") : require("https"); | ||
| const body = JSON.stringify({ text: process.env.HOOK_SYNC_TEXT }); | ||
| const req = lib.request(u, { method: "POST", headers: { "content-type": "application/json", "content-length": Buffer.byteLength(body) } }, (res) => res.resume()); | ||
| req.on("error", () => {}); | ||
| req.write(body); | ||
| req.end(); | ||
| ' 2>/dev/null || log "WARN: webhook post failed" |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Webhook request has no timeout — a slow/unreachable URL hangs the container.
lib.request(...) has no timeout option. If HOOK_SYNC_WEBHOOK_URL points to a slow or unresponsive endpoint, the node process blocks indefinitely. Since notify is called before exit 64 (lines 60-61) and at run completion (lines 132-134), a misconfigured webhook can prevent the container from ever exiting, causing the CronJob to hang until its deadline kills it.
⏱️ Proposed fix: add a 5s timeout and destroy on timeout
- const req = lib.request(u, { method: "POST", headers: { "content-type": "application/json", "content-length": Buffer.byteLength(body) } }, (res) => res.resume());
+ const req = lib.request(u, { method: "POST", timeout: 5000, headers: { "content-type": "application/json", "content-length": Buffer.byteLength(body) } }, (res) => res.resume());
req.on("error", () => {});
+ req.on("timeout", () => req.destroy());
req.write(body);📝 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.
| HOOK_SYNC_TEXT="$2" node -e ' | |
| const { URL } = require("url"); | |
| const u = new URL(process.env.HOOK_SYNC_WEBHOOK_URL); | |
| const lib = u.protocol === "http:" ? require("http") : require("https"); | |
| const body = JSON.stringify({ text: process.env.HOOK_SYNC_TEXT }); | |
| const req = lib.request(u, { method: "POST", headers: { "content-type": "application/json", "content-length": Buffer.byteLength(body) } }, (res) => res.resume()); | |
| req.on("error", () => {}); | |
| req.write(body); | |
| req.end(); | |
| ' 2>/dev/null || log "WARN: webhook post failed" | |
| HOOK_SYNC_TEXT="$2" node -e ' | |
| const { URL } = require("url"); | |
| const u = new URL(process.env.HOOK_SYNC_WEBHOOK_URL); | |
| const lib = u.protocol === "http:" ? require("http") : require("https"); | |
| const body = JSON.stringify({ text: process.env.HOOK_SYNC_TEXT }); | |
| const req = lib.request(u, { method: "POST", timeout: 5000, headers: { "content-type": "application/json", "content-length": Buffer.byteLength(body) } }, (res) => res.resume()); | |
| req.on("error", () => {}); | |
| req.on("timeout", () => req.destroy()); | |
| req.write(body); | |
| req.end(); | |
| ' 2>/dev/null || log "WARN: webhook post failed" |
🤖 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 `@docker/entrypoint.sh` around lines 47 - 56, The webhook POST in the
entrypoint’s Node snippet has no timeout, so a slow or unreachable
HOOK_SYNC_WEBHOOK_URL can block container shutdown. Update the request logic
inside the notify flow to set a short timeout (for example 5s) on lib.request,
and ensure the request is destroyed/aborted when the timeout fires so the
process can continue to exit. Keep the existing error handling and logging path
in place so notify still falls back to log WARN: webhook post failed.
| case "$WORKSPACE" in | ||
| ""|"/"|"."|"..") log "ERROR: refusing unsafe WORKSPACE '$WORKSPACE'"; notify fail "hook-sync: unsafe WORKSPACE '$WORKSPACE'"; exit 66 ;; | ||
| esac |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
WORKSPACE safety check is too narrow for the rm -rf at line 77.
The case statement only blocks exact matches of "", /, ., ... It doesn't protect $HOME, /home, /etc, /usr, or other system directories. If WORKSPACE is misconfigured to $HOME (where git config --global and gh auth state live, lines 67-69), the find ... -exec rm -rf {} + at line 77 destroys those credentials after auth setup, causing confusing downstream failures.
🛡️ Proposed fix: add `$HOME` and common system dirs to the blocklist
case "$WORKSPACE" in
- ""|"/"|"."|"..") log "ERROR: refusing unsafe WORKSPACE '$WORKSPACE'"; notify fail "hook-sync: unsafe WORKSPACE '$WORKSPACE'"; exit 66 ;;
+ ""|"/"|"."|".."|"$HOME"|"$HOME/"|"/home"|"/home/"|"/etc"|"/usr"|"/var"|"/bin"|"/sbin"|"/lib")
+ log "ERROR: refusing unsafe WORKSPACE '$WORKSPACE'"; notify fail "hook-sync: unsafe WORKSPACE '$WORKSPACE'"; exit 66 ;;
esac📝 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.
| case "$WORKSPACE" in | |
| ""|"/"|"."|"..") log "ERROR: refusing unsafe WORKSPACE '$WORKSPACE'"; notify fail "hook-sync: unsafe WORKSPACE '$WORKSPACE'"; exit 66 ;; | |
| esac | |
| case "$WORKSPACE" in | |
| ""|"/"|"."|".."|"$HOME"|"$HOME/"|"/home"|"/home/"|"/etc"|"/usr"|"/var"|"/bin"|"/sbin"|"/lib") | |
| log "ERROR: refusing unsafe WORKSPACE '$WORKSPACE'"; notify fail "hook-sync: unsafe WORKSPACE '$WORKSPACE'"; exit 66 ;; | |
| esac |
🤖 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 `@docker/entrypoint.sh` around lines 72 - 74, The WORKSPACE guard in
entrypoint.sh is too narrow for the destructive cleanup later in the script.
Extend the case block that validates WORKSPACE to also reject $HOME and common
system directories such as /home, /etc, and /usr before the rm -rf path in the
hook-sync flow. Keep the existing unsafe-path check structure, but broaden the
blocklist in the same WORKSPACE validation logic so unsafe values are caught
before git config, gh auth, and the cleanup step run.
"docker" was misleading — the folder holds the hook-sync image, not generic Docker config. Updates build-image.yml (context / file / path-trigger) and the CHANGELOG reference. The docker/*-action GitHub Actions org refs are untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gYNtmxTPnTyeQeUmEHA28
…ename The docker/ -> docker-hook-sync/ rename staged as a pure rename, so this one-line Build-command comment update landed as a separate unstaged edit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012gYNtmxTPnTyeQeUmEHA28
|
🔍 Automated code review started — analyzing [{"additions":87,"deletions":0,"path":".github/workflows/build-image.yml"},{"additions":0,"deletions":69,"path":".github/workflows/sync-hook-events.yml"},{"additions":0,"deletions":3,"path":".gitignore"},{"additions":3,"deletions":0,"path":"CHANGELOG.md"},{"additions":75,"deletions":0,"path":"docker-hook-sync/Dockerfile"},{"additions":109,"deletions":0,"path":"docker-hook-sync/entrypoint.sh"},{"additions":222,"deletions":0,"path":"scripts/sync-agent-cli-harnesses-prompt.md"},{"additions":0,"deletions":179,"path":"scripts/sync-hook-events-prompt.md"}] files, +496/-251... ⏱️ This may take a few minutes. Results will be posted here when complete. |
| # The daily rebuild refreshes the @latest-pinned claude-code + failproofai npm globals. | ||
|
|
||
| on: | ||
| push: |
There was a problem hiding this comment.
🟡 Warning — the new image and entrypoint get zero automated validation on PRs.
build-image.yml triggers only on push to main, schedule, and workflow_dispatch — there is no pull_request trigger. So this PR's Dockerfile, entrypoint.sh, and this workflow's own YAML are never built or exercised by CI here; a broken image first surfaces post-merge (on the push trigger) or on the next daily cron. There are also no unit tests covering entrypoint.sh (env-gating, the jq neutralization, branch cutting) — CLAUDE.md's "always add unit tests for new behaviour" isn't met for the new shell logic.
Fix: add a build-only PR trigger so the image is smoke-tested before merge, e.g.
on:
pull_request:
paths: ['docker-hook-sync/**', '.github/workflows/build-image.yml']and gate publishing so PR runs build-without-push:
push: ${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.push_to_ghcr) }}At minimum, run bash -n / shellcheck on entrypoint.sh in the existing CI.
Note: I could not build the image during this review — no Docker daemon in the sandbox, non-root user, and sudo is blocked by the repo's own dogfood policy — so the image build itself remains verified only by the author's local run.
| CFG=".failproofai/policies-config.json" | ||
| if [ -f "$CFG" ]; then | ||
| cfg_tmp="$(mktemp)" | ||
| jq '.enabledPolicies |= map(select(. != "require-ci-green-before-stop" and . != "block-read-outside-cwd"))' "$CFG" > "$cfg_tmp" && mv "$cfg_tmp" "$CFG" |
There was a problem hiding this comment.
🔵 Info — the policy-neutralization step fails open if jq errors.
Under set -euo pipefail, a command that fails on the left of && does not abort the script (only the command after the final && triggers errexit). I confirmed this empirically:
$ bash -c 'set -euo pipefail; false > /tmp/x && mv /tmp/x /tmp/y; echo REACHED'
REACHED # script did NOT abort
So if jq here ever exits non-zero (malformed config, or .enabledPolicies missing / not an array → Cannot iterate over null), the && mv is skipped, $CFG is left unmodified, and the run proceeds with require-ci-green-before-stop + block-read-outside-cwd still active — exactly the two policies this line means to drop. The result is a confusing half-sabotaged run instead of a fast, obvious failure. It won't trigger with today's valid committed config, but it's a latent fail-open in a gating-relevant step.
Fix: check jq explicitly rather than relying on &&:
if ! jq '.enabledPolicies |= map(select(. != "require-ci-green-before-stop" and . != "block-read-outside-cwd"))' "$CFG" > "$cfg_tmp"; then
log "ERROR: failed to neutralize policies in $CFG"; exit 67
fi
mv "$cfg_tmp" "$CFG"| - failproofai's own Stop hooks are active in this checkout. If you make commits, you | ||
| will not be allowed to stop until you have **committed**, **pushed**, and **opened a | ||
| PR** for them. If you make **no** commits (nothing to sync, or you only commented on | ||
| an already-open sync PR), a clean working tree stops fine — so on those paths, do |
There was a problem hiding this comment.
🔵 Info — require-commit-before-stop gates on any dirty tree, not just "commits ahead".
The clean-stop paths described here rely on the working tree being pristine. But requireCommitBeforeStop denies whenever git status --porcelain is non-empty (src/hooks/builtin-policies.ts:1102) — that includes untracked files, independent of whether the branch is ahead of main. The committed-config edit is safely hidden by git update-index --skip-worktree, so that's covered. The gap: if a subagent writes any stray scratch / temp / notes file during a no-drift or comment-only run, the tree is dirty and this gate denies the Stop, forcing an unwanted commit → push → PR on a path that's meant to end quietly.
Suggestion: on the no-op / comment-only paths, explicitly forbid creating untracked files (or git stash -u / clean before stopping).
Aside: the PR description's "these three only enforce when the branch has commits ahead of origin/main" is accurate for require-push-before-stop / require-pr-before-stop, but not for require-commit-before-stop.
| ## 0.0.12-beta.0 — 2026-07-09 | ||
|
|
||
| ### Features | ||
| - Add the `hook-sync` container (`docker-hook-sync/` + `.github/workflows/build-image.yml`, published to `ghcr.io/failproofai/hook-sync`): a single-shot daily job that clones failproofai and runs Claude Code in `--effort ultracode` mode to detect hook event-name, tool/payload-schema, and settings-file-shape drift across all seven integrated agent CLIs, then opens one auto-sync PR — the agent commits, pushes, and opens the PR itself, gated by failproofai's own `require-*-before-stop` hooks (dogfooding). Replaces the broken `sync-hook-events.yml` GitHub Action (removed, along with `scripts/sync-hook-events-prompt.md`). |
There was a problem hiding this comment.
🔵 Nit — CHANGELOG entry omits the PR number.
The repo convention (CLAUDE.md → Changelog) is: "Each entry should be a single line: a short description followed by the PR number (e.g. - Add foo support (#123))." This entry ends at …prompt.md). with no (#483). The sibling Codex ### Fixes entry in the same section omits it too, so it's a consistent drift — worth fixing both.
Cosmetic only: it won't fail the version-consistency CI check (that compares packages/*/package.json vs root, not the CHANGELOG). package.json version 0.0.12-beta.0 correctly matches the ## 0.0.12-beta.0 — 2026-07-09 heading. ✅
🔍 Automated Code Review📋 Executive SummaryThis PR replaces the ~70-day-broken 📊 Change Architecturegraph TD
GHA["build-image.yml 🟢<br/>push / schedule / dispatch"] -->|build & push| IMG["ghcr.io/failproofai/hook-sync 🟢"]
CRON["k8s CronJob<br/>(infra, not this repo)"] -->|daily run| IMG
IMG --> EP["entrypoint.sh 🟢<br/>env-gate → clone --depth=1 → branch<br/>→ jq-neutralize 2 policies → PTY-wrap"]
EP -->|"--effort ultracode -p"| CLA["Claude Code (headless)"]
PROMPT["sync-agent-cli-harnesses-prompt.md 🟢<br/>fan-out 1 subagent × 7 CLIs"] --> CLA
CLA --> DEC{"drift? open sync PR?"}
DEC -->|no drift| NOOP["clean stop · no PR"]
DEC -->|PR already open| CMT["comment missing drift only"]
DEC -->|drift + no PR| ONEPR["edit → commit → push → ONE PR"]
ONEPR -.enforced by.-> GATES["require-commit / push / pr-before-stop 🟡"]
OLD["sync-hook-events.yml + prompt 🔴 DELETED"]
style GHA fill:#90EE90
style IMG fill:#90EE90
style EP fill:#90EE90
style PROMPT fill:#90EE90
style OLD fill:#FFB6C1
style GATES fill:#FFD700
Legend: 🟢 New · 🔴 Removed · 🟡 Dogfood enforcement gate 🔴 Breaking Changes✅ None. Purely additive infra (Dockerfile, entrypoint, workflow, prompt) plus removal of the dead/broken
|
hermes-exosphere
left a comment
There was a problem hiding this comment.
Careful, security-conscious infra. I verified the mechanism end-to-end short of the container build (no Docker daemon / non-root / sudo blocked by this repo's own dogfood policy — see the summary):
- ✅ dogfood hooks run in a bare
--depth=1clone (hook import graph is node-builtin + relative only — nobun installneeded) - ✅
jqneutralization keeps exactlyrequire-commit/push/pr-before-stopand dropsrequire-ci-green-before-stop+block-read-outside-cwd;--skip-worktreekeeps the edit out of the PR - ✅ the autonomous prompt's repo references (7 arrays / 5 maps / 8 tool-maps / 2 test anchors / 8 fixtures) are all accurate
- ✅ no breaking changes, no stale refs, version matches CHANGELOG
No correctness bugs or merge blockers. The one thing worth doing before/with merge is the top inline finding — add a build-only pull_request trigger so this image (never built in PR CI) is smoke-tested. The other three inline notes are low-severity hardening + a CHANGELOG (#483) nit. Leaving as a comment (not a block); full detail in the summary comment and inline threads.
What
Adds
hook-sync: a single-shot container that runs daily to keep failproofai in sync with every integrated agent CLI. It replaces the broken.github/workflows/sync-hook-events.ymlGitHub Action, which had failed every run since 2026-04-30 (~70 days) — the LiteLLM proxy behindANTHROPIC_BASE_URLrejects thecontext_managementfield the auto-updated Claude Code CLI now sends (400 Extra inputs are not permitted).What it does
Each run clones failproofai and invokes
claude --effort ultracode --model claude-opus-4-8to fan out one subagent per CLI (Claude, Codex, Copilot, Cursor, OpenCode, Pi, Gemini) and detect three classes of drift:*HOOK_EVENT_TYPESarrays +*EVENT_MAPs insrc/hooks/types.ts(what the old workflow covered).*_TOOL_MAP/*_TOOL_INPUT_MAPtables +src/hooks/policy-evaluator.tsbranches.src/hooks/integrations.ts+ the committed dogfood fixtures (the PR fix(codex): drop invalid top-levelversionfield + correct timeout unit (seconds) #482 class: e.g. Codex's invalid top-levelversionfield / wrong timeout unit).Dedupe: the agent checks for an already-open sync PR before editing anything. If one is open it does not open a second PR — it comments the missing drift on that PR (or does nothing if fully covered). Otherwise it makes the edits, commits, pushes, and opens one PR itself — all gated by failproofai's own
require-commit/push/pr-before-stophooks running inside the container (dogfooding). Auth isCLAUDE_CODE_OAUTH_TOKEN(no proxy) +GH_TOKEN.Files
docker-hook-sync/Dockerfile—node:20-bookworm-slim+bun+gh/jq/git/dumb-init+claude-code@latest+failproofai@latest.docker-hook-sync/entrypoint.sh— env-gate → clone → fresh branch → policy neutralization →claude --effort ultracode(PTY-wrapped for stream-json). Output streams to the pod log.scripts/sync-agent-cli-harnesses-prompt.md— the ultracode prompt (3 scopes × 7 CLIs; fan-out → dedupe → synthesis + one PR / comment)..github/workflows/build-image.yml— builds/pushesghcr.io/failproofai/hook-sync(push todocker-hook-sync/**+ daily rebuild +workflow_dispatch)..github/workflows/sync-hook-events.yml+scripts/sync-hook-events-prompt.md; retired their.gitignorescratch line.The k8s CronJob that schedules the image lives in our infra, not this repo.
The one non-obvious mechanism
The repo's
.failproofai/policies-config.jsonenablesrequire-ci-green-before-stopandblock-read-outside-cwd. Both would sabotage the run — the first blocks on the intentionally red CI of map-bearing event additions (which need a human to add the*EVENT_MAPentry), the second blocks the agent from reading the settings fixtures it must edit. The entrypointjq-drops just those two from the throwaway clone's config andgit update-index --skip-worktrees the file, so the edit never enters the PR while the threerequire-*-before-stopgates stay active. Those three only enforce when the branch has commits ahead oforigin/main, so the comment-only / nothing-to-sync paths stop cleanly.Verification
Done locally: image builds;
claude 2.1.205(≥ 2.1.203,--effortflag present),bun 1.3.14,gh,jq,git,script,unzip,dumb-initall present; the entrypoint gates on missing env (exit 64); thejqneutralization keeps the three PR gates and drops exactly the two targets.Needs tokens/infra (not runnable in review): a scoped live smoke run — start with a prompt scoped to one CLI, on a fork — then wire the image into the k8s CronJob. Secret
hook-sync-secretskeys:CLAUDE_CODE_OAUTH_TOKEN,GH_TOKEN.Notes
claude-opus-4-8per request; a daily ~7-subagent ultracode fan-out on Opus is not cheap —CLAUDE_MODELoverrides it.🤖 Generated with Claude Code
https://claude.ai/code/session_012gYNtmxTPnTyeQeUmEHA28