Invisible git sync for development. An auto-commit, multi-mirror daemon that watches your repos, commits every change with deterministic, facts-based messages, and pushes to GitHub, GitLab, and Codeberg simultaneously.
This page is the user guide for dracon-sync (also rendered on
crates.io). This repo is the canonical, live source on main
(mirrored to GitLab and Codeberg). For workspace builds it is also
checked out as a nested standalone repo under dracon-sync/ in the
dracon-utilities
parent repo — a regular nested repo, not a submodule.
repos separates the last observed push result from current convergence
(queued, working, retrying, excluded, blocked, stalled, paused or unknown).
Historical push success does not prove newer edits are published; unchanged idle
repos remain clean. An upstream commit's age is not a push timestamp.
See the health legend and evidence rules
or run dracon-sync repos --legend.
cargo install dracon-syncThe binary lands at ~/.cargo/bin/dracon-sync. The shipped systemd unit runs %h/.local/bin/dracon-sync, so
for service use either copy it there or install from a checkout:
# Clone the repo
git clone https://github.com/DraconDev/dracon-sync-background-auto-commit-multi-remote.git
cd dracon-sync-background-auto-commit-multi-remote
# Build (locked)
cargo build --release --locked
# Install where the service unit looks
install -d "$HOME/.local/bin"
install -m 0755 target/release/dracon-sync "$HOME/.local/bin/dracon-sync"Or, from a full dracon-utilities checkout, run ./install.sh at the
parent root to install all three utilities plus services and hooks in
one pass.
Other tools solve parts of the problem:
- git-auto-sync: Auto-commits a single repo, no mirroring
- gitea-mirror: One-way mirror to a single Forgejo instance, no auto-commit
- git-bridge: Multi-provider sync, but no auto-commit or AI
- swarf: Invisible sync for AI agents, but only a side-band directory
dracon-sync is the only tool that combines all of these into one daemon:
| Capability | git-auto-sync | gitea-mirror | git-bridge | swarf | dracon-sync |
|---|---|---|---|---|---|
| Auto-commit on change | ✅ | ❌ | ❌ | ✅ | ✅ |
| Multi-repo watch | ❌ | ✅ | ✅ | ❌ | ✅ |
| Multi-provider mirror | ❌ | ✅ (→1) | ✅ | ❌ | ✅ (3+) |
| Deterministic commit messages | ❌ | ❌ | ❌ | ❌ | ✅ |
| Version bump + release | ❌ | ❌ | ❌ | ❌ | ✅ |
| Safety guards | ❌ | ❌ | ❌ | ❌ | ✅ |
| Visibility sync | ❌ | ❌ | ❌ | ❌ | ✅ |
| Broken tracking repair | ❌ | ❌ | ❌ | ❌ | ✅ |
The AI (or human) works on one repo at a time, makes changes, and sync handles the rest — the AI never needs to think about commits, pushes, or cross-repo coordination.
- You edit files
- Sync detects changes within seconds
- After a brief inactivity delay (5s default), sync commits with a deterministic message
- Pushes to origin (GitHub) and all mirror remotes (GitLab, Codeberg)
- Done — no manual git commands needed
GitHub origin uses HTTPS with Personal Access Tokens — more reliable than SSH (no agent timeouts, no key rotation). GitLab and Codeberg mirrors use SSH by default, with HTTPS PAT fallback on SSH failures.
When auto_github_private = true, newly initialized repos without an origin remote automatically get a private GitHub repository created via gh, with the remote added and initial commit pushed.
- Monitors repositories for changes across watched roots
- Commits, pulls, and pushes automatically based on policy
- Respects freeze markers (e.g., during deployments)
- Detects and repairs common git issues (conflicted remotes, stuck pushes)
- Repairs broken upstream tracking refs (e.g.
origin/master: gone) - Consolidates dual main/master branch repos to main
- Manages permanently stuck repos
- Prunes stale operational state on daemon restart (stuck repos, incident ledger, visibility cache)
Daemon commits go through libgit2, which never runs git hooks — so a repo's
pre-commit guard cannot fire on them. dracon-sync closes that gap two ways:
- Forward-only bucket guard (
bucket_guard.rs): before every auto-commit, the daemon walks the checkout and its ancestors for an executableweb/scripts/bucket-strategy-guard.sh(the same script.githooks/pre-commitruns for human commits) and runs the staged forward-only check itself. A violation — or a failure of the guard infrastructure — blocks the commit with the index intact, staging and worktree content retained, and the row is reported as blocked. Repos with no discoverable guard script are unaffected. Override the script location withDRACON_BUCKET_GUARD; an override pointing at a missing path fails closed rather than skipping. This is what prevents the silent protected-asset deletions (hellhunterf5d55b5, deathrun30352ba) that previously only detonated at push time. - Stuck-push routing (
stuck_route.rs): when a repo stays push-stuck and the cause is a repo-policy refusal a loop can act on (a hook/guard decision, not a transient network failure), the daemon appends one append-only NOTE row to the owning loop's own ledger (<repo>/.pi-glla/audit-loop/findings.md) carrying the guard's violation output. One row per episode (keyed by HEAD tip), only after 30 minutes stuck, and never for self-healing network/forge outages — those stay daemon-internal. Repos without a ledger file are skipped silently.
Deterministic facts extracted from the diff. No AI, no LLM, no prose. Routing
keys are grep-searchable via git log --grep=.
One tray item, never a pile. Action-required states (stuck pushes, degraded
mirrors, blocked/unowned repos) feed a single "Dracon Sync: N issues" notification,
updated in place when the set changes, silent when it doesn't, and auto-replaced
with an all-clear when everything resolves. Declared forge/network outages are
named first in the body. The alerts log (dracon-sync-alerts.jsonl) keeps the full
per-repo paper trail; statistical notices (pile growth, dispatch starvation) are
transient popups, never sticky.
Run the repository installer from the repository root:
cd dracon-utilities
./install.shThis will:
- Build the release binary
- Install to
~/.local/bin/dracon-sync - Set up and start the systemd user service
The per-utility directories do not contain standalone installers; use the root install.sh for all utilities.
# Build
cargo build --release --locked -p dracon-sync
# Copy binary
cp target/release/dracon-sync ~/.local/bin/
# Install systemd service
mkdir -p ~/.config/systemd/user
cp dracon-sync.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable dracon-sync.service
systemctl --user start dracon-sync.servicedracon-sync repos displays the latest commit author through Git's mailmap.
Use a repository .mailmap or Git's mailmap.file setting to map your
confirmed aliases to one identity. For example:
DraconDev <dracsharp@gmail.com> endless-td-dev <endless-td@dracon.local>
This changes the displayed name while preserving the original commit author,
hash, and timestamp. Configure user.name and user.email for new commits;
those settings do not change previous authors. Mailmaps are display mappings;
the sync policy's trusted identities are configured separately.
# Show policy path, watched roots, and discovered repos
dracon-sync status
# One-shot sync across all discovered repos
dracon-sync once
# Run continuous sync daemon (default: 1s pulse interval)
dracon-sync daemon
# Override the pulse interval from CLI
dracon-sync daemon --interval-secs 30
# Sync a specific repository now
dracon-sync sync-now ~/Dev/my-project
# Sync repos currently reported as WARN (dirty-only triage)
dracon-sync sync-now --warns
dracon-sync sync-now --warns --dry-run
# Edit the sync policy
dracon-sync config edit
# Validate the sync policy
dracon-sync config validate
# Report across all repos
dracon-sync repos
dracon-sync repos --only-concern
dracon-sync repos --json
# Repair concern repos (dry-run by default)
dracon-sync repair concerns
dracon-sync repair concerns --apply
# Repair warn repos
dracon-sync repair warns
dracon-sync repair warns --apply
# Manage stuck repos
dracon-sync repair stuck-list
dracon-sync repair stuck-unstuck ~/Dev/repo
# Manage dual-branch repos
dracon-sync repair dual-branch-list
dracon-sync repair dual-branch-repair ~/Dev/repo
# Repair orphan origin URLs (e.g. after remote rename)
dracon-sync repair origins
dracon-sync repair origins --apply
# Refresh the visibility cache for every watched repo
dracon-sync refresh-visibility
# Flip a repository public across github + gitlab (codeberg skipped by
# default, to protect the 85 GiB grace quota; add --include-codeberg)
dracon-sync make-public ~/Dev/repo
dracon-sync make-private ~/Dev/repo
# Scaffold standard files (LICENSE, optional .github/FUNDING.yml, ...)
# FUNDING.yml is Dracon-specific; external users must opt in explicitly.
dracon-sync scaffold
dracon-sync scaffold --repo ~/Dev/repo --files LICENSE
dracon-sync scaffold --repo ~/Dev/repo --files '.github/FUNDING.yml' --dry-run
# Manually publish to registries
dracon-sync publish run ~/Dev/repo
dracon-sync publish status ~/Dev/repo
# Check daemon health and metrics
dracon-sync health
dracon-sync metrics
# Print version
dracon-sync --version# Check status
systemctl --user status dracon-sync.service
# View logs
journalctl --user -u dracon-sync -f
# Restart after config changes
systemctl --user restart dracon-sync.serviceNever systemctl stop the daemon for git surgery — use
dracon-sync maintenance -- <cmd> instead (it pauses, runs the command,
and always resumes).
The live config lives at ~/.dracon/utilities/sync/dracon-sync.toml
(dracon-sync config edit / dracon-sync config validate). The annotated
template is dracon-sync.example.toml in this repo
(dracon-sync/dracon-sync.example.toml from the monorepo root):
[sync]
# Watch directories for git repositories
watch_roots = ["/home/user/Dev", "/home/user/work"]
# Pulse interval in seconds (how often to scan for changes)
pulse_interval_secs = 1
# Delay after last change before auto-push (seconds)
inactivity_push_delay_secs = 5
# Auto git operations
auto_commit = true
auto_pull = true
auto_push = true
auto_bump_versions = true
# Auto-repair concerns and warnings
auto_repair_concerns = true
auto_repair_warns = true
# Automatic private GitHub remote creation
# When a repo has no origin remote, creates a private GitHub repo,
# adds the HTTPS remote, and pushes the initial commit.
auto_github_private = true
auto_github_private_account = "YourOrgOrUsername"
# Exclude specific repos or directories
exclude_repos = ["/home/user/Dev/archived"]
exclude_dir_names = ["node_modules", "target", ".venv"]If auto_github_private = true in your policy, any git init in a watched root will automatically:
- Create a private GitHub repo via
gh repo create --private - Add the HTTPS remote:
git remote add origin https://github.com/account/repo.git - Push the initial commit:
git push -u origin HEAD
Requirements:
ghCLI must be installed and authenticated (gh auth login)auto_github_private_accountmust match your GitHub username or org
Push to multiple providers simultaneously. GitHub uses HTTPS + PAT; others use SSH with HTTPS fallback:
[[remotes]]
name = "github"
push_url = "https://github.com/DraconDev/{repo}.git"
[[remotes]]
name = "gitlab"
push_url = "git@gitlab.com:dracondev/{repo}.git"
auto_create = true
[[remotes]]
name = "codeberg"
push_url = "git@codeberg.org:dracondev/{repo}.git"
auto_create = false # Codeberg/Forgejo doesn't support push-to-createStore PATs for HTTPS fallback and API operations:
# GitLab
echo "GITLAB_TOKEN=<token>" > ~/.dracon/utilities/sync/secrets/gitlab.env
# Codeberg
echo "CODEBERG_TOKEN=<token>" > ~/.dracon/utilities/sync/secrets/codeberg.envCommit messages are deterministic facts extracted from the diff — no AI, no inference.
[INTENT] | N file(s) in DIRS [files] DELTA:+A/-B [METRICS]
- INTENT — task state transitions from markdown (
CLOSED: task1, task2orWIP: task1), capped at 10 tasks - DIRS — top-level directories touched (root files show no
inclause) - DELTA — lines added/removed
- METRICS —
TEST:,NEW:,DEL:,DEPS:,BIN:,TESTONLY:,ENV:,TAG:,MERGE:,REVERT:
LLM-scribed commit messages were removed — they hallucinated context and the AI reads the diff anyway. Mechanical facts are searchable (git log --grep="JWT"), honest, and compact.
On daemon start/restart, sync prunes stale operational state:
- Stuck repos: Removes entries from stuck-push tracking for repos no longer stuck
- Incident ledger: Enforces retention policy (keeps last N entries per
incident_retention) - Alert ledger: Keeps a JSONL history of desktop-sync alert attempts under
~/.local/state/dracon/dracon-sync-alerts.jsonl - Visibility cache: Removes orphan
.lastfiles for repos no longer watched - Broken tracking: Repairs
origin/master: gonerefs → re-points toorigin/{branch} - Stale index.lock: Removes
.git/index.lockfiles with no holding process (left by crashed git operations). Without this, a stale lock blocks all git operations in that repo.
Broken tracking repair also runs every ~300 cycles (~5 min) in the daemon loop, since new :gone tracking breaks can appear at runtime.
Push timeouts: code defaults are push_op_timeout_secs = 300,
pull_op_timeout_secs = 30, repo_sync_timeout_secs = 420,
push_retries = 3 (all progress-aware: active transfers extend the idle
deadline). The fleet operator config overrides the push timeout to 900s.
A hanging mirror push blocks the daemon for that repo until it times out,
so tune these down on flaky networks rather than up.
Filter-only cooldown: Repos with clean/smudge filter changes (e.g. dracon-warden encryption) show as dirty in git status but have no diff after staging. The daemon detects this, resets the staging area, and applies a cooldown to prevent tight re-check loops.
Fingerprint-based scheduling: The daemon uses a fingerprint (branch + effective_dirty + staged + ahead + behind) to determine if a repo needs syncing. Only after the fingerprint stays stable for inactivity_push_delay_secs (default 5s) does the daemon attempt a sync.
When a push fails, dracon-sync follows this decision tree to recover:
Push Attempt
├── SSH push with hardening (ConnectTimeout=10, ConnectionAttempts=2)
│ ├── Success → Done ✅
│ └── Failure → Continue
├── Retry loop (configurable retries, linear backoff 1-5s)
│ ├── Success → Done ✅
│ └── Failure → Continue
├── Transport fallback (SSH → HTTPS)
│ ├── GitHub HTTPS (no token needed for public repos)
│ ├── GitLab HTTPS (requires GITLAB_TOKEN)
│ ├── Codeberg HTTPS (requires CODEBERG_TOKEN)
│ ├── Success → Done ✅
│ └── All fail → Continue
└── Final failure handling
├── Diverged (ahead > 0 AND behind > 0) → Mark as stuck, skip
├── Clean + ahead > 0 + 3 failures → Mark as stuck, skip
└── Other → Log incident, continue to next repo
| Failure Type | Cause | Recovery |
|---|---|---|
| SSH timeout | Network issue, SSH agent not running | HTTPS fallback, retry |
| SSH auth failed | Expired key, permission denied | HTTPS fallback with PAT |
| HTTPS auth failed | Expired/missing PAT | Check token in secrets/ |
| Non-fast-forward | Remote has diverged | Merge/pull or manual resolution; stuck tracking prevents repeated failed pushes |
| Rejected | Branch protection, permission denied | Manual intervention needed |
| Network unreachable | DNS failure, firewall | Retry with backoff |
| Timeout | Hanging connection, large repo | Progress-aware push/pull timeout; failed operations retry or surface in repos/health |
A repo is marked as "stuck" when:
- Diverged:
ahead > 0 AND behind > 0— requires manual resolution - Clean + ahead + failures: Repo has no uncommitted changes, has unpushed commits, and push has failed 3+ times — indicates a permanent issue (deleted remote, permission denied, etc.)
Stuck repos are tracked in ~/.local/state/dracon/dracon-sync-stuck-push-repos.json and skipped until manually unstuck:
dracon-sync repair stuck-unstuck /path/to/repoThe repos command shows real dirty file counts from libgit2's get_status(), not filtered counts. The OK/WARN/CONCERN status uses has_sync_relevant_dirty_entries() (which excludes target/, node_modules/, oversized files, etc.), but the MOD/STG columns always show the actual number of modified/staged files. Previously, when effective_dirty was false (all changes excluded by policy), the report showed 0 — making repos with dozens of uncommitted files appear clean.
src/— utility source codetests/— integration testsCargo.toml— standalone build manifest with registry dependenciesREADME.md— this utility's user guidedracon-sync.example.toml— example configai.example.toml,providers.example.json— AI/credential config templatesdracon-sync.service— systemd user-service unitscripts/— release + install-verification toolingLICENSE,SECURITY.md,.gitignore,.github/— repo metadata- Architecture + invariants:
docs/SOURCE_OF_TRUTH.md - Design notes:
BLUEPRINT.md
| Boundary | Decision |
|---|---|
| Source code | This repo (main branch) |
| Source of truth | This repo; mirrored to GitLab and Codeberg; nested checkout under dracon-sync/ in the dracon-utilities parent |
| Workspace integration | Builds standalone, or as a dracon-utilities workspace member when nested under dracon-sync/ |
| Shared libraries | Published dracon-git crate from crates.io |
| Operational policy | ~/.dracon/utilities/ TOML files |
The descriptive name is a deliberate choice for Codeberg/Forgejo, where
descriptive repo names get upvotes and free attention because readers
immediately know what the project does. The full word list (no fillers, no
audience/UX claims) is documented in
docs/design/github-feature-repos.md.
Watches configured repositories, waits for changes to settle (fingerprint stability / debounce), commits deterministic diff-based messages, and pushes to origin plus configured mirrors. Invisible: runs in the background, no user interaction required.
- Binary:
dracon-sync - Service: dracon-sync.service (
systemctl --user enable --now dracon-sync.service; neversystemctl stopit — usedracon-sync maintenance -- <cmd>for git surgery) - Example policy:
dracon-sync.example.tomlin this repo (dracon-sync/dracon-sync.example.tomlfrom thedracon-utilitiesmonorepo root); the live config lives at~/.dracon/utilities/sync/dracon-sync.toml(dracon-sync config edit/dracon-sync config validate) - Common commands:
dracon-sync status · dracon-sync repos · dracon-sync health · dracon-sync daemon; alsosync-now,pause/resume/maintenance,once,config,repair,ownership,scan-bloat— full list atdracon-sync --help
Changes are made here, on main. The GitLab/Codeberg mirrors follow
automatically, as does the nested checkout in the dracon-utilities parent.
AGPL-3.0-only — see LICENSE.
Part of the Dracon developer workspace.
The selective automatic LFS adoption plan proposes standard Git LFS with a configurable S3-backed LFS service (OVH first). Small repos stay in ordinary Git; eligible assets are routed under growth/file safety rules, with database/secret exclusions and Warden protection. This is a plan, not deployed automatic offload. It defines shared GitHub/GitLab storage, consumer bootstrap, recovery tests and transition from the custom preview below.
Source builds now provide read-only storage plan and storage validate
commands. They explain configurable placement rules, operator-approved backend
bindings, existing filters, and optional Git history measurements. They do not
automatically upload or migrate files. See preview usage
and the implementation roadmap.
Source builds also provide explicit protected-manifest import, private asset
recovery and Linux checkout hydration with local crash resume. These operations
remain an unreleased preview. A signed S3 HTTP adapter is tested in source;
explicit S3 recovery uses a private operator credential vault. Explicit
storage probe-backend checks competing creates and conflicting-write refusal;
storage capture retains a policy-selected new path as an exact-version job;
storage capture-update retains a new version of an already enrolled path
under its recorded contract; storage advance-job prepares a captured version
and verifies required copies without changing Git. Automatic routing and
independent-provider certification remain unfinished.
The preview usage above documents the required bindings and limitations.
Standard Git LFS v1 with SHA-256 is the proposed replacement for the custom
preview above. Status: isolated qualification milestones tested; production routing
unfinished; the broader rollout qualification is not satisfied. The implementation/qualification report
docs/design/lfs-adoption-implementation-report-2026-10-11.md
documents every step.
What is verified on this host (every check is bounded, isolated, and run green):
- Policy evaluator (
src/lfs_policy.rs) — 11 typed-evidence tests cover pressure/file/churn routing, stickyRetainLfs, opt-out, override resolution, invalid-threshold fail-closed, and source-text vs. approved asset classification.with_overrideis tested in isolation; there is no live daemon routing integration. - Real-file classifier (
src/lfs_classify.rs) — 8 fixtures build realEvidencefrom real files (200 MiB small-image stays in Git;.pemand.dbrefusal regardless of size; SQLite-magic refusal for extensionless payloads;.sqlmigrations asSourceText; sparse 101 MiB asset selection; sticky representation surviving real file shrinkage; per-repo override moving the individual-trigger threshold). Sizes use sparse writes so the fixtures stay disk-light whilemetadata().len()reports the real measurement. Bounded run:cargo test -p dracon-sync --lib --locked lfs_→ 19 passed, 0 failed. - Dispatcher prototype (
scripts/lfs_dispatcher_prototype.py) — 12 tests cover guard-before-transfer, exact args/ref replay, multi-ref/ SHA-256/deletion syntax, NUL-argv/env refusal, recursion, transfer failure, shared deadline, and post-refusal successful invocation. Not the global Warden hook. - Warden source-only hook ordering (
dracon-warden/src/main.rsPRE_PUSH_HOOK) — 33 hook tests confirm foreign-before-local chaining runs only after all refs pass the existing checks. No setup/harden/ install command was run; the live global hook is unchanged. - Stock Git LFS 3.8.0 (loopback) — 7 tests cover canonical pointer
encoding, >100 MiB sparse fixture staging, smudge, malformed-pointer
refusal, and empty-file handling. Verified release archive SHA-256:
e455e00f15d9b95661b8d53498ffb0c3367962cf1ec73c31ab7369516cd6ab8d. - Gitea 28.1.0 (loopback, disposable) — 2 tests cover authenticated
batch/upload/download, anonymous refusal, cross-repository OID refusal,
and stock-client round-trip with a fresh consumer cache. Verified binary
SHA-256:
ea93cfa4a300564ac457d3903469fea49ec5a4da37d0cebaa435f8e329ea64d0. Gitea 28.1.0 is the first isolated-test candidate, not an approved deployment.
What is not yet implemented or verified:
- No production LFS server is installed or qualified.
- No private OVH binding or scoped IAM principal has been qualified.
- No live filter, hook, remote, or git config was modified. The
~/.config/git/hooks/pre-pushlive hook is unchanged. - The Warden hook chain order is fixed in source but is not installed live; a stock LFS pre-push still needs payload authorization before any upload.
- No recovery story is implemented. Independent backup of LFS objects and required server metadata is not present.
- README, operator docs, and live-configuration evidence stop at the isolated-verified level above. Anything that would require a live host, live IAM, live hooks, or fresh forge clones is documented as a concrete unblocker in the report and is not shipped.
What you can do today:
- Run the bounded checks listed above (every command, every expected count, every SHA-256 is in the report).
- Read the implementation/qualification report for the full evidence chain and the six concrete unblockers needed to advance to the next milestone.
What requires operator authorization before continuing:
- Install a maintained LFS server on an authorized host (Gitea 28.1.0 first candidate).
- Provision a private OVH bucket and a dedicated least-privilege IAM
principal. Do not reuse the public
dracon-mastercredentials. - Stand up an independent backup of LFS objects and server metadata.
- Install and authorize the live Warden hook with payload authorization before any chained hook.
- Install
git-lfson this host and on a separate fresh consumer host. - Approve a single non-secret binary fixture under a deliberate path
in the chosen pilot game (no
git lfs migrate, no fleet-wide enrollment).
These boundaries are preserved on purpose. The custom storage preview
above remains a preview, not a production qualification claim. The standard
LFS milestone supplies isolated test code, not a deployment. Retained workspace
tests passed; short clippy/release logs show dracon-sync output and exit 0,
but omit invocation scope and do not prove full workspace qualification.
See the report for bounded commands, limitations and fresh binary-dependent
run receipts (evidence/lfs/stock-client.log and evidence/lfs/gitea-api.log).
Both verified binaries are required for the server suite; absent prerequisites
error rather than skip. Live pilot,
independent server recovery, application staging authorization, and the
complete daemon/manual integration matrix remain unverified.
