Skip to content

[OPIK-6998] [CI] feat: keep agent configs in sync across main and worktrees - #7396

Draft
JetoPistola wants to merge 1 commit into
mainfrom
danield/OPIK-6998-claude-config-sync
Draft

[OPIK-6998] [CI] feat: keep agent configs in sync across main and worktrees#7396
JetoPistola wants to merge 1 commit into
mainfrom
danield/OPIK-6998-claude-config-sync

Conversation

@JetoPistola

@JetoPistola JetoPistola commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Details

Keeps Claude/Cursor/Codex agent configs fresh from .agents/ so skills don't drift stale after teammates merge updates, and makes git worktrees work (they previously loaded zero skills). .agents/ stays the single source of truth — vendored output is not committed.

  • make agent-configs regenerates only the surfaces a user opted into (detected by .claude/ / .cursor / .codex presence) for whatever tree it runs in. Each worktree owns its own .claude/ regenerated from its own .agents/, so a skill-editing branch sees that branch's skills — not main's. (A Claude Code session reads its own tree's .claude/; verified.)
  • Drift detection: a committed .agents/.sync-manifest (deterministic source hash) travels with every branch/worktree. make check-agent-configs fails loudly when .agents/ changed but wasn't re-synced, or when opted-in output is missing. Runs in pre-commit (on .agents/ changes) and CI — stale skills can't silently ship.
  • Local post-merge / post-checkout hooks auto-run the sync (best-effort; installed via a separate make agent-hooks, never in CI). Shared core.hooksPath guard via one require_no_hookspath macro.
  • convert-mcp.sh never overrides an existing .mcp.json (generated only on first setup) so personal MCP tokens are never destroyed. Entity-aware reconciliation of new upstream servers → follow-up OPIK-7271.
  • .claude/rules is left to the branch's own tracked files; rule content is migrating to Agent Skills → follow-up OPIK-7272.

Design note: composed from established patterns — source-hash manifest, per-tree regeneration, and CI drift-check — since .agents-vendoring has no single off-the-shelf recipe. Skills hot-reload in a running session (only .mcp.json needs a restart), so once files are fresh on disk they propagate immediately.

Change checklist

  • User facing
  • Documentation update

Issues

  • OPIK-6998

AI-WATERMARK

AI-WATERMARK: yes

  • Tools: Claude Code
  • Model(s): Claude Opus 4.8
  • Scope: full implementation
  • Human verification: code review (comet-cra reviewer pass), research into Claude Code's config-loading model + industry sync patterns, manual + CI testing

Testing

Local (macOS) + CI:

  • scripts/test-agent-configs.sh (plain bash, throwaway temp repos, never the real checkout), wired into .github/workflows/agent_configs_tests.yml: covers first-setup vs never-override MCP, opt-in no-op detection, manifest drift-detection (edit .agents/ → check fails → re-write → passes), and per-worktree independence (a worktree resolves to its own tree, not main).
  • make check-agent-configs verified: in-sync passes, .agents/ edit without re-sync fails with the "run make agent-configs" nudge, manifest is deterministic across recomputes (CI-safe).
  • Pre-commit drift hook (🤖🔄 agent configs in sync) fires on .agents/ changes; actionlint passes.
  • Earlier: convert-mcp no-override, agent-hooks install/core.hooksPath guard, real worktree regeneration.

Not run: Java/FE/SDK suites — no files in those areas changed.

Documentation

N/A — developer-tooling change; make help, in-file comments, and a shareable architecture diagram (diagrams/opik-6998-diagram.html) document the model. User-facing docs unaffected.

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

⏱️ pre-commit per-hook timing

Hook Description Result Duration
⚙️ actionlint — github workflows Lint GitHub Actions workflows 0.76s
🤖🔄 agent configs in sync 0.08s
Total (2 ran) 0.84s
⏭️ 40 skipped (no matching files changed)
Hook Description Result
🐍 trim trailing whitespace — python sdk Strip trailing whitespace ⏭️
🐍 fix end of files — python sdk Ensure files end in a newline ⏭️
🐍 ruff — python sdk Lint + autofix Python (ruff) ⏭️
🐍 ruff-format — python sdk Format Python code (ruff) ⏭️
🐍 mypy — python sdk Static type check ⏭️
🤖 trim trailing whitespace — optimizer Strip trailing whitespace ⏭️
🤖 fix end of files — optimizer Ensure files end in a newline ⏭️
🤖 check yaml — optimizer Validate YAML syntax ⏭️
🤖 check json — optimizer Validate JSON syntax ⏭️
🤖 check toml — optimizer Validate TOML syntax ⏭️
🤖 check for added large files — optimizer Block large files (>1MB) ⏭️
🔐 detect private key — optimizer Block committed private keys ⏭️
🤖 check for merge conflicts — optimizer Block merge-conflict markers ⏭️
🤖 check for case conflicts — optimizer Block case-only name clashes ⏭️
🤖 pyupgrade — optimizer Modernize Python syntax ⏭️
🤖 ruff — optimizer Lint + autofix Python (ruff) ⏭️
🤖 ruff-format — optimizer Format Python code (ruff) ⏭️
🤖 mypy — optimizer Static type check ⏭️
📓 nbstripout — optimizer notebooks Strip notebook output ⏭️
📝 markdownlint — optimizer Lint Markdown ⏭️
🔤 codespell — optimizer Fix common misspellings ⏭️
📊 radon cc — optimizer Cyclomatic-complexity gate ⏭️
📊 radon raw — optimizer Raw size metrics gate ⏭️
📊 xenon — optimizer Fail on complexity thresholds ⏭️
📊 lizard — optimizer Cyclomatic-complexity gate ⏭️
🧹 vulture — optimizer Find dead code ⏭️
🛡️ trim trailing whitespace — guardrails Strip trailing whitespace ⏭️
🛡️ fix end of files — guardrails Ensure files end in a newline ⏭️
🛡️ ruff — guardrails Lint + autofix Python (ruff) ⏭️
🛡️ ruff-format — guardrails Format Python code (ruff) ⏭️
🛡️ mypy — guardrails Static type check ⏭️
⚓ helm-docs Regenerate Helm chart README ⏭️
block non-public FE plugins Block non-public FE plugins ⏭️
☕ spotless — java backend Format Java code ⏭️
🧪 pre-commit wrapper smoke tests Self-test the wrapper scripts ⏭️
🌐 eslint — frontend Lint + autofix JS/TS ⏭️
🌐 typecheck — frontend Whole-project tsc type check ⏭️
📘 eslint — typescript sdk Lint + autofix JS/TS ⏭️
📘 typecheck — typescript sdk Whole-project tsc type check ⏭️
🐳 hadolint — dockerfiles Lint Dockerfiles ⏭️

Warning

Lint failed. Fix locally, then push again:

pip install pre-commit   # or: brew install pre-commit
make hooks               # install the git hook (once per clone)
make precommit           # run the same checks on your changed files

Formatters fix files in place — review the changes, git add, and commit.
Or apply the diff shown in the failed CI leg directly.

Comment thread scripts/sync-agent-configs.sh Outdated
Comment thread scripts/sync-agent-configs.sh Outdated
@JetoPistola
JetoPistola force-pushed the danield/OPIK-6998-claude-config-sync branch from 5032cc3 to 753fb3f Compare July 8, 2026 04:05
Comment thread .hooks/post-checkout Outdated
@JetoPistola
JetoPistola force-pushed the danield/OPIK-6998-claude-config-sync branch from 753fb3f to 305d807 Compare July 8, 2026 04:13
Comment thread Makefile
Comment thread Makefile Outdated
Comment thread Makefile Outdated
@JetoPistola
JetoPistola force-pushed the danield/OPIK-6998-claude-config-sync branch from 305d807 to 80da46a Compare July 8, 2026 04:19
Comment thread .hooks/post-merge Outdated
@JetoPistola
JetoPistola force-pushed the danield/OPIK-6998-claude-config-sync branch 6 times, most recently from 2fe6cd3 to af092df Compare July 8, 2026 05:01
@JetoPistola
JetoPistola force-pushed the danield/OPIK-6998-claude-config-sync branch from af092df to ea929db Compare July 26, 2026 20:44
Comment thread .hooks/post-merge
Comment on lines +5 to +8
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
cd "$REPO_ROOT" || exit 0

command -v make >/dev/null 2>&1 && make agent-configs || true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Duplicated hook bodies drift

.hooks/post-mergeduplicates the repo-root resolution andmake agent-configstail from.hooks/post-checkout, so changes to the sync command, error handling, or repository lookup have to be made twice — should we factor that shared tail into scripts/agent-configs-hook.shand leave the checkout-specific guard inpost-checkout`?

Severity

Want Baz to fix this for you? Activate Fixer

Comment on lines +52 to +58
# Manifest matches source. If a surface is opted in but its output is
# missing, this tree was pulled/created without regenerating.
if [[ -d ".claude" ]] && [[ ! -d ".claude/skills" ]]; then
echo "⚠️ .claude/ present but skills not generated in this tree." >&2
echo " Run 'make agent-configs' to regenerate." >&2
exit 1
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Validator ignores Cursor/Codex outputs

make check-agent-configs only verifies .claude/skills after matching .agents/.sync-manifest, so breaking .cursor, .codex, or AGENTS.override.md still passes the check even though scripts/sync-agent-configs.sh regenerates those surfaces — should we validate them too?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
scripts/agent-configs-manifest.sh around lines 52-58 inside the `check` case, the
opted-in output validation only checks `.claude`/`.claude/skills` and misses broken
`.cursor`, `.codex`, and `AGENTS.override.md` outputs. Refactor this section to add
additional assertions that mirror the targets/conditions in
scripts/sync-agent-configs.sh (i.e., if those surfaces are expected to exist in this
tree, verify the corresponding `.cursor` symlink/dir, `.codex` symlink/dir, and
`AGENTS.override.md` file are present; otherwise print the same “Run `make
agent-configs` to regenerate” error and exit 1). This ensures deleting/breaking those
outputs while leaving `.agents/.sync-manifest` unchanged will no longer incorrectly pass
`make check-agent-configs`.

@JetoPistola
JetoPistola force-pushed the danield/OPIK-6998-claude-config-sync branch from ea929db to 0e64a14 Compare July 26, 2026 20:51
Comment thread Makefile
Comment on lines +46 to +54
define require_no_hookspath
@hp=$$(git config --get core.hooksPath || true); \
if [ -n "$$hp" ]; then \
echo "Error: core.hooksPath is set to '$$hp'; hooks in .git/hooks would be ignored."; \
echo " Clear it, then re-run 'make $(1)':"; \
echo " git config --unset core.hooksPath # local (this repo)"; \
echo " git config --global --unset core.hooksPath # if it was set globally"; \
exit 1; \
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hook-path guard checks wrong repository

require_no_hookspath runs git config --get core.hooksPath in Make’s current directory while HOOKS_DEST is rooted at $(MAKEFILE_DIR), so make -f /path/to/repo/Makefile agent-hooks from another repo can pass the guard for the caller repo and still install into a target repo that ignores .git/hooks — should we run the config lookup from $(MAKEFILE_DIR) too?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In Makefile around
lines 46-54, update the `require_no_hookspath` macro so its `git config --get
core.hooksPath` lookup runs in the target repository used to compute `HOOKS_DEST` (i.e.,
anchored to `$(MAKEFILE_DIR)`), not in Make’s current working directory. Refactor the
macro to execute the git command with `-C $(MAKEFILE_DIR)` (or by doing `cd
"$(MAKEFILE_DIR)"` before running `git config`). Then verify `hooks` and `agent-hooks`
still call the macro unchanged, but now the guard correctly fails when the target
repo’s `core.hooksPath` is set.

Comment thread .hooks/post-checkout
Comment on lines +8 to +11
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
cd "$REPO_ROOT" || exit 0

command -v make >/dev/null 2>&1 && make agent-configs || true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Branch checkout enables code execution

The hook runs make agent-configs from the checked-out tree, which reaches ./scripts/sync-agent-configs.sh in Makefile:142-143, so a malicious commit can execute branch-controlled code on checkout before users review it; the same issue exists in .hooks/post-merge. Should we use a trusted pinned implementation outside the branch, or add an explicit trust/confirmation boundary before running it?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
.hooks/post-checkout around lines 8-11 (the REPO_ROOT cd + `make agent-configs`
invocation), avoid executing branch-controlled scripts from the freshly checked-out
working tree. Refactor so the hook runs the sync logic from a trusted, immutable
location (e.g., the hooks’ installed directory or a vendored script path with a fixed
revision/checksum) rather than whatever `Makefile`/`./scripts/sync-agent-configs.sh` the
branch provides. Add the same protection to .hooks/post-merge and, if you can’t fully
pin, introduce an explicit trust/confirmation gate (e.g., require user confirmation or
refuse to run when the branch/commit isn’t trusted) before calling `make
agent-configs`.

…ktrees

Regenerate opted-in agent-config surfaces (Claude/Cursor/Codex) from each
tree's own .agents/, and detect drift so stale skills can't silently ship.
.agents/ stays the single source of truth; vendored output is not committed.

- make agent-configs: regenerate opted-in surfaces (detected by folder
  presence) for whatever tree it runs in — each worktree owns its own .claude/
  from its own .agents/, so a skill-editing branch sees its own skills
- committed .agents/.sync-manifest (source hash) + make check-agent-configs:
  fails loudly when .agents/ changed but wasn't re-synced, or opted-in output
  is missing; runs in pre-commit (on .agents/ changes) and CI
- post-merge/post-checkout hooks auto-run the sync (best-effort convenience),
  installed via a separate make agent-hooks (never CI); shared core.hooksPath
  guard via one require_no_hookspath macro
- convert-mcp.sh never overrides an existing .mcp.json (first-setup only);
  entity-aware reconciliation deferred to OPIK-7271
- .claude/rules left to the branch's own tracked files (rules -> Skills,
  OPIK-7272)
- sandbox test suite (temp repos, never the real checkout) + CI workflow

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@JetoPistola
JetoPistola force-pushed the danield/OPIK-6998-claude-config-sync branch from 0e64a14 to f9131da Compare July 26, 2026 21:00
Comment thread Makefile
Comment on lines +195 to +206
# Install the local post-merge/post-checkout hooks. Kept separate from `make
# hooks` (the pre-commit framework) because agent-config sync must never run in CI.
agent-hooks:
@if [ -z "$(HOOKS_DEST)" ]; then \
echo "Error: $(MAKEFILE_DIR) is not in a git repository."; \
exit 1; \
fi
$(call require_no_hookspath,agent-hooks)
@cd "$(MAKEFILE_DIR)" && \
if [ ! -d "$(HOOKS_SRC)" ]; then echo "Error: $(MAKEFILE_DIR)/$(HOOKS_SRC)/ does not exist."; exit 1; fi && \
if [ ! -d "$(HOOKS_DEST)" ]; then echo "Error: $(HOOKS_DEST)/ does not exist."; exit 1; fi && \
for h in post-checkout post-merge; do \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hook synchronization can regress silently

The new agent-hooks install flow and .hooks/post-checkout/.hooks/post-merge runtime behavior don't have focused regression coverage here, so changes could silently break automatic synchronization. Should we add a shell/integration test that covers the correct worktree/common hooks directory, core.hooksPath refusal, $3=0 skips, and make agent-configs on branch checkout and merge?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In Makefile around
lines 195-209, the new `agent-hooks` target (and its installed `.hooks/post-checkout` /
`.hooks/post-merge` behavior) is not covered by regression tests. Add a focused
shell/integration test that creates a temporary git repo/worktree, runs `make
agent-hooks`, and asserts the hook files are installed into the computed git common
hooks directory (`GIT_COMMON_DIR/hooks`) rather than the repo’s local `.git/hooks`;
also assert the command fails with a clear error when `core.hooksPath` is set. Extend
the same test to execute the installed hook scripts with scenarios that cover “branch
checkout and merge invoke `make agent-configs`” and the skip case where the hook
receives `$3=0`, using a spy/stub that records whether `agent-configs` (or
`scripts/sync-agent-configs.sh`) was invoked. Place the test in the project’s existing
test harness (e.g., a new script under `scripts/` or `tests/`) and ensure it runs in CI
so future changes can’t silently break syncing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants