---
title: "A /sync Command Turned Into a Workflow Audit"
canonical: https://dxdev.com/blog/2026-04-14_sync-skill-workflow-audit/
datePublished: 2026-04-14
---
The agent hit a missing skills index in the first minute and offered to build one. I told it to go ahead. That was the last straightforward decision I made that day.

The `/sync` skill was supposed to be a simple warm-up: pull a shared skills index, confirm hook configs were current across my four local platform repos, check the dev board wasn't stale. One command any Claude Code session could call to put itself in a known state before real work started. The agent's first discovery, that no single canonical skills index existed, turned out to be the first of several.

## The repos had four versions of the truth

My four local clones of the platform each had a `.claude/skills/` directory. They had started from the same scaffold and diverged over months as I patched one without touching the others: skill files with different names for the same operation, hook references pointing at paths that no longer existed, `CLAUDE.md` files describing a tooling layout the repos had since outgrown. None of it was broken badly enough to surface as an error. It was just inconsistent, and inconsistency at the skills level means the agent behaves differently depending on which clone it starts in.

The agent built the index, flagging conflicts as it went rather than picking one version arbitrarily. Clone two had a skill that clone three had superseded under a different name. The base clone had a hook configuration that predated and partially overlapped what the other three repos were using. Half a dozen conflicts surfaced that I had never known existed, because each repo worked fine in isolation and I had never compared them directly with a shared index in mind.

## The startup scaffolding disagreed across clones

With the skills index settled, I ran a pass through the rest of the startup scaffolding: env templates, `mcp.json`, and the `CLAUDE.md` at the root of each repo. All four had drifted. Some env templates still pointed at defaults from a layout that had been restructured months earlier. The `mcp.json` files had diverged enough that a session starting on the base clone loaded a different tool set than the same session starting on clone four, with no error and no explanation for why the two sessions behaved differently.

This matters because sessions inherit everything from startup hooks. If a hook on one clone reaches for a path that only exists on that clone, the session either fails silently or throws an error that looks like it came from whatever the session was trying to do. That error pattern had been showing up for months, and I kept tracing it to the wrong source, because the error message never pointed at the hook.

## The JIRA base URL was wrong in the reference config

When `/sync` tried to pull vault session summaries and check their state, I noticed the JIRA ticket links in those summaries were broken. They pointed to `an internal host` instead of the actual Atlassian host. The summaries looked correct because the ticket IDs were formatted right and appeared in the expected places. The links just went nowhere useful.

One field in the JIRA reference config, the base URL, had never been updated after the initial integration setup. It was still `an internal host`, a placeholder from an early planning doc that predated the real JIRA instance. Every session that had ever pulled from that config had been generating broken links, appending valid ticket IDs to the wrong domain. I had been reading session summaries for months without clicking through, because the IDs looked plausible. What the summaries were actually producing was unusable evidence. The field itself, once found, was a one-line fix.

## Tags were not where the version was

The `/sync` skill needed to report each clone's current deployed version so session summaries could flag whether a given clone was current. The agent's first pass read the latest git tag. That worked until I tested it against a repo that had just shipped a hotfix.

Hotfix releases tag master after the merge, but the tag sometimes lands on the merge commit and sometimes on the commit immediately before it, depending on which tooling ran the release flow. Reading the tag gave back a SHA with an unclear position in the branch history. The version number looked plausible, and a plausible version with an ambiguous lineage is exactly the kind of answer that fails silently at the worst possible time.

I rewrote the lookup to read merge commits directly: find the most recent commit on master whose message starts with "Merge branch 'hotfix/", then parse the version from the branch name. It is less elegant than reading a tag, but tags are labels someone attached after the fact; merge commits are structural facts in the history and they do not move.

## The safeguard was why /item looked frozen

The last thing I changed that day was not a fix. It was a disable.

A `check-learnings` hook fires at the end of every `/item` session. Its job is to review what happened, extract anything worth keeping, and propose additions to the shared learning doc, so nothing useful gets lost when a session closes. The intent is sound. The problem was that the hook called an endpoint that could take up to 20 seconds to respond, and the `/item` close sequence had no timeout. When the endpoint was slow, the close just hung. The session looked frozen from the outside, and nothing moved until the endpoint finally responded or I gave up and killed the process.

For weeks I had assumed the problem was in `/item` itself, some bug in the close logic I hadn't caught yet. The symptom was too consistent to ignore but showed up irregularly, roughly one session in three, which made it hard to reproduce cleanly.

Disabling the `check-learnings` hook fixed it in under a second. The hook has been off since then, waiting on a rewrite that adds a proper timeout.

The lesson it was meant to preserve was getting lost anyway, because `/item` never finished.
