Skip to content

[failproofai-483] Add hook-sync container for daily agent-CLI drift detection#483

Merged
NiveditJain merged 4 commits into
mainfrom
luv-483
Jul 9, 2026
Merged

[failproofai-483] Add hook-sync container for daily agent-CLI drift detection#483
NiveditJain merged 4 commits into
mainfrom
luv-483

Conversation

@NiveditJain

@NiveditJain NiveditJain commented Jul 9, 2026

Copy link
Copy Markdown
Member

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.yml GitHub Action, which had failed every run since 2026-04-30 (~70 days) — the LiteLLM proxy behind ANTHROPIC_BASE_URL rejects the context_management field 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-8 to fan out one subagent per CLI (Claude, Codex, Copilot, Cursor, OpenCode, Pi, Gemini) and detect three classes of drift:

  1. Event names — the *HOOK_EVENT_TYPES arrays + *EVENT_MAPs in src/hooks/types.ts (what the old workflow covered).
  2. Tool / payload schema — the *_TOOL_MAP / *_TOOL_INPUT_MAP tables + src/hooks/policy-evaluator.ts branches.
  3. Settings-file shape — the literals in src/hooks/integrations.ts + the committed dogfood fixtures (the PR fix(codex): drop invalid top-level version field + correct timeout unit (seconds) #482 class: e.g. Codex's invalid top-level version field / 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-stop hooks running inside the container (dogfooding). Auth is CLAUDE_CODE_OAUTH_TOKEN (no proxy) + GH_TOKEN.

Files

  • docker-hook-sync/Dockerfilenode: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 neutralizationclaude --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/pushes ghcr.io/failproofai/hook-sync (push to docker-hook-sync/** + daily rebuild + workflow_dispatch).
  • Deleted .github/workflows/sync-hook-events.yml + scripts/sync-hook-events-prompt.md; retired their .gitignore scratch 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.json enables require-ci-green-before-stop and block-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_MAP entry), the second blocks the agent from reading the settings fixtures it must edit. The entrypoint jq-drops just those two from the throwaway clone's config and git update-index --skip-worktrees the file, so the edit never enters the PR while the three require-*-before-stop gates stay active. Those three only enforce when the branch has commits ahead of origin/main, so the comment-only / nothing-to-sync paths stop cleanly.

Verification

Done locally: image builds; claude 2.1.205 (≥ 2.1.203, --effort flag present), bun 1.3.14, gh, jq, git, script, unzip, dumb-init all present; the entrypoint gates on missing env (exit 64); the jq neutralization 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-secrets keys: CLAUDE_CODE_OAUTH_TOKEN, GH_TOKEN.

Notes

  • Model is pinned to claude-opus-4-8 per request; a daily ~7-subagent ultracode fan-out on Opus is not cheap — CLAUDE_MODEL overrides it.
  • Headless ultracode fan-out is documented but worth confirming on the first live run; a single-agent loop over the 7 CLIs is the fallback.

🤖 Generated with Claude Code

https://claude.ai/code/session_012gYNtmxTPnTyeQeUmEHA28

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
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@NiveditJain, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 11 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ffb83384-2ffd-4782-94c8-c106904e21ef

📥 Commits

Reviewing files that changed from the base of the PR and between 10d3a2f and 0ee6d7b.

📒 Files selected for processing (5)
  • .github/workflows/build-image.yml
  • CHANGELOG.md
  • docker-hook-sync/Dockerfile
  • docker-hook-sync/entrypoint.sh
  • scripts/sync-agent-cli-harnesses-prompt.md
📝 Walkthrough

Walkthrough

Adds a new build-image GitHub Actions workflow and Docker image/entrypoint script implementing a daily hook-sync container job, plus a new multi-agent CLI sync prompt. Removes the old sync-hook-events.yml workflow, its prompt script, and a related .gitignore entry; updates CHANGELOG.md.

Changes

Hook-sync automation replacement

Layer / File(s) Summary
build-image GitHub Actions workflow
.github/workflows/build-image.yml
New workflow builds/publishes the hook-sync container to GHCR on push, cron, or manual dispatch, with tag computation and conditional push logic.
Dockerfile for hook-sync image
docker/Dockerfile
New image based on Node 20, installs git/gh/CLI tooling and global npm packages, sets up a non-root user, and configures dumb-init entrypoint with bun.
entrypoint.sh runtime sync logic
docker/entrypoint.sh
New entrypoint script validates env vars, authenticates with GitHub, clones and branches the repo, edits policy config, runs claude via PTY, and posts webhook telemetry.
New multi-CLI sync prompt
scripts/sync-agent-cli-harnesses-prompt.md
New runbook prompt defines drift detection and PR orchestration for syncing seven agent CLI harnesses.
Removal of old sync-hook-events workflow and cleanup
.github/workflows/sync-hook-events.yml, scripts/sync-hook-events-prompt.md, .gitignore, CHANGELOG.md
Removes the old sync-hook-events workflow, its prompt, and a .gitignore rule; adds a 0.0.12-beta.0 CHANGELOG entry documenting the change.

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
Loading

Poem

A rabbit hops through YAML and shell,
Old sync workflows bid farewell 🐇
A container wakes each break of day,
Clones, checks drift, then hops away,
One tidy PR, telemetry sent —
Burrow's clean, and well content!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The PR description is detailed, but it omits the template's Type of Change and Checklist sections. Add the required Type of Change section and the checklist items for lint, tsc, tests, and build, and align the heading structure with the template.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly names the new hook-sync container and its daily drift-detection purpose.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment on lines +49 to +54
- name: Log in to GHCR
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

why do we need this? we can login with the repo itself?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Comment on lines +78 to +87
- 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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

also add latest tag

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

: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.

Comment thread docker/entrypoint.sh Outdated
Comment on lines +45 to +57
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"
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

we do not need for now.. lets remove... no dead code.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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

@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: 3

🧹 Nitpick comments (2)
.github/workflows/build-image.yml (1)

37-87: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Add a concurrency group to prevent simultaneous builds.

A push to docker/** around 08:00 UTC can overlap with the cron rebuild. Both builds would race to push ghcr.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-permissions removes 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

📥 Commits

Reviewing files that changed from the base of the PR and between a33ac72 and 10d3a2f.

📒 Files selected for processing (8)
  • .github/workflows/build-image.yml
  • .github/workflows/sync-hook-events.yml
  • .gitignore
  • CHANGELOG.md
  • docker/Dockerfile
  • docker/entrypoint.sh
  • scripts/sync-agent-cli-harnesses-prompt.md
  • scripts/sync-hook-events-prompt.md
💤 Files with no reviewable changes (3)
  • scripts/sync-hook-events-prompt.md
  • .gitignore
  • .github/workflows/sync-hook-events.yml

Comment on lines +3 to +7
# 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 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"
fi

Repository: 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

Comment thread docker/entrypoint.sh Outdated
Comment on lines +47 to +56
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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 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.

Suggested change
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.

Comment on lines +72 to +74
case "$WORKSPACE" in
""|"/"|"."|"..") log "ERROR: refusing unsafe WORKSPACE '$WORKSPACE'"; notify fail "hook-sync: unsafe WORKSPACE '$WORKSPACE'"; exit 66 ;;
esac

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 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.

Suggested change
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.

NiveditJain and others added 2 commits July 9, 2026 02:38
"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
@NiveditJain NiveditJain merged commit 00cddcb into main Jul 9, 2026
12 checks passed
@hermes-exosphere

Copy link
Copy Markdown

🔍 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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 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.

Comment thread CHANGELOG.md
## 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`).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 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. ✅

@hermes-exosphere

Copy link
Copy Markdown

🔍 Automated Code Review

📋 Executive Summary

This PR replaces the ~70-day-broken sync-hook-events.yml Action with a self-contained hook-sync container: a daily single-shot job that clones the repo, neutralizes two policies in the throwaway clone, and runs claude --effort ultracode to fan out one subagent per integrated CLI (7), detecting event-name / tool-schema / settings-shape drift and opening (or commenting on) exactly one sync PR — all gated by failproofai's own require-*-before-stop hooks (dogfooding). The engineering is careful and security-conscious: SHA-pinned actions, least-privilege permissions, validated inputs, and an autonomous prompt whose repo references I verified are 100% accurate. No breaking changes to code, API, DB, serialization, or the published npm package. Findings are all minor / hardening — nothing merge-blocking.


📊 Change Architecture

graph 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
Loading

Legend: 🟢 New · 🔴 Removed · 🟡 Dogfood enforcement gate


🔴 Breaking Changes

None. Purely additive infra (Dockerfile, entrypoint, workflow, prompt) plus removal of the dead/broken sync-hook-events.yml + its prompt and a stale .gitignore line. No src/** .ts changed; the npm package/API/DB/serialization are untouched. Version 0.0.12-beta.0 matches the CHANGELOG heading. Grep confirmed no dangling references to the removed artifacts or to the old docker/ path (the docker/ → docker-hook-sync/ rename is clean).


⚠️ Issues Found

  1. 🟡 Warning.github/workflows/build-image.yml:14 — no pull_request trigger, so the Dockerfile + entrypoint.sh are never built/run in PR CI (and have no unit tests). A broken image only surfaces post-merge / on cron. (inline)
  2. 🔵 Infodocker-hook-sync/entrypoint.sh:77jq … > tmp && mv fails open under set -e: if jq errors, neutralization is silently skipped and the run proceeds with the two "dropped" policies still active. (inline, empirically confirmed)
  3. 🔵 Infoscripts/sync-agent-cli-harnesses-prompt.md:19require-commit-before-stop denies on any dirty tree (incl. untracked scratch files), not just "commits ahead"; a stray temp file on a no-op/comment run would block the clean stop. (inline)
  4. 🔵 NitCHANGELOG.md:6 — entry omits the required (#483) PR number (repo convention). (inline)

🔬 Logical / Bug Analysis

  • Dogfood works in a bare shallow clone (verified). The --hook path (bin/failproofai.mjssrc/hooks/handler.ts → evaluator/builtins → lib/telemetry-id) imports only node builtins + relative modules — no third-party deps — so a git clone --depth=1 with no bun install genuinely runs the hooks. The Dockerfile comment is accurate.
  • Neutralization is exactly scoped (verified). Ran the actual jq filter against the committed config: output keeps require-commit/push/pr-before-stop and drops require-ci-green-before-stop + block-read-outside-cwd. git update-index --skip-worktree correctly hides the edit from git status, so it never enters the PR and never trips require-commit-before-stop.
  • Gate logic is sound. require-push/pr-before-stop short-circuit when origin/main..HEAD is empty (clone sets up origin/main; entrypoint re-fetches it) → comment/no-op paths stop cleanly; the full-drift path stops only after commit+push+PR.
  • Autonomous prompt references all check out (verified): 7 *HOOK_EVENT_TYPES, 5 *EVENT_MAP, 8 *_TOOL(_INPUT)_MAP, both test anchors (installs hooks for all 28 event types, gemini.eventTypes …toHaveLength(11)), and all 8 dogfood fixtures exist. Important for an unsupervised editor.
  • Security hygiene is strong: SHA-pinned actions, persist-credentials: false, permissions: {contents: read, packages: write}, tag_suffix regex-validated (^[A-Za-z0-9_.-]{1,128}$) and passed via env: (no ${{ }} shell injection). Tokens are exported but never logged; no set -x.
  • entrypoint.sh guardrails: env-gate (exit 64), unsafe-WORKSPACE guard (exit 66), missing-prompt guard (exit 65), script -e propagates claude's rc. bash -n passes; mktemp … .sh suffix works on GNU coreutils.

🧪 Evidence — Build & Test Results

Static validation (run in this review)
entrypoint.sh          : bash -n → syntax OK
build-image.yml        : YAML valid; jobs=[build]; triggers=[push, schedule, workflow_dispatch]
jq neutralization      : keeps require-commit/push/pr-before-stop; drops require-ci-green + block-read-outside-cwd  ✅
mktemp .sh suffix      : /tmp/run-claude.c2VKBQ.sh  ✅
set -e && fail-open     : `false > x && mv` under set -euo pipefail → "REACHED" (does NOT abort)  ⇒ finding #2
hook import graph      : src/hooks/** + lib/telemetry-id → node builtins + relative only (no node_modules needed)  ✅
prompt references      : 7 arrays / 5 maps / 8 tool-maps / 2 test anchors / 8 fixtures → all present  ✅
version consistency    : package.json 0.0.12-beta.0 == CHANGELOG heading  ✅
stale refs             : sync-hook-events* and old docker/ path → none found  ✅
Docker image build — NOT run
Could not exercise `docker build docker-hook-sync/` in this sandbox:
  • docker daemon not running
  • reviewer is non-root (uid 1001); dockerd needs root
  • `sudo` is blocked by the repo's own dogfood block-sudo policy
The image build is therefore verified only by the author's stated local run.
This is exactly why finding #1 (add a build-only pull_request trigger) matters.

🔗 Issue Linkage

⚠️ No issue linked (CodeRabbit's "Linked Issues" check was skipped for the same reason). The PR body is thorough and self-documenting; a tracking issue for the pending k8s-CronJob wiring + first live smoke run would aid traceability.


💡 Suggestions (non-blocking)

  • The daily run now lives in external infra. Merging this PR builds the image but does not by itself restore the sync — the k8s CronJob must be wired up separately (PR acknowledges this). Until then there is no scheduled sync in-repo.
  • --effort ultracode headless fan-out and CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS are unverified runtime assumptions (author flagged both). The single-agent-loop-over-7-CLIs fallback is a sensible contingency; consider making it the default until the fan-out is confirmed on a live run.
  • @latest-pinned claude-code means a future upstream rename of --effort/flags could break the run; the daily rebuild is both the mitigation and the exposure. Consider pinning a known-good major and bumping deliberately.
  • First live run should be scoped to one CLI on a fork (as the PR plans) before enabling the full 7-way fan-out against the real repo with a push-capable GH_TOKEN.

🏆 Verdict

⚠️ APPROVED WITH SUGGESTIONS — No correctness bugs or breaking changes; the mechanism is verified sound end-to-end (short of the container build, which the sandbox can't run). The one actionable ask is a build-only pull_request trigger so this never-CI-tested image gets smoke-tested before merge; the rest are low-severity hardening notes and a CHANGELOG nit. None block merge.


Automated code review · 2026-07-09 · verified statically; Docker build not runnable in sandbox

@hermes-exosphere hermes-exosphere 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.

⚠️ Approved with suggestions — non-blocking.

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=1 clone (hook import graph is node-builtin + relative only — no bun install needed)
  • jq neutralization keeps exactly require-commit/push/pr-before-stop and drops require-ci-green-before-stop + block-read-outside-cwd; --skip-worktree keeps 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.

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