Skip to content

Git Sync

Git Sync synchronizes local DAG files and agent files (memory, skills, souls) with a remote Git repository.

Tracked Files and Item IDs

Git Sync tracks items by itemId — the file path relative to the DAGs directory, without extension.

Local fileitemIdkind
my-dag.yamlmy-dagdag
subdir/report.ymlsubdir/reportdag
memory/MEMORY.mdmemory/MEMORYmemory
memory/dags/my-dag/MEMORY.mdmemory/dags/my-dag/MEMORYmemory
skills/my-skill/SKILL.mdskills/my-skill/SKILLskill
souls/persona.mdsouls/personasoul

File Scanning Rules

Implemented in internal/gitsync/service.go.

Remote scan

Includes files with extensions .yaml, .yml, and .md. Files with .md extension are only accepted when the item ID starts with memory/, skills/, or souls/.

Local untracked scan

Discovers local files not yet in sync state:

  • DAGs: .yaml and .yml files in the root of {dags_dir}/. Flat scan, not recursive.
  • Memory: any .md file under memory/. Recursive walk through all subdirectories.
  • Skills: only skills/<name>/SKILL.md. Scans one level of subdirectories under skills/, looking specifically for SKILL.md in each.
  • Souls: souls/*.md. Flat scan, not recursive.

Configuration

yaml
git_sync:
  enabled: true
  repository: github.com/your-org/dags
  branch: main
  path: ""  # subdirectory in repo (empty = root)
  push_enabled: true

  auth:
    type: token
    token: ${GITHUB_TOKEN}

  auto_sync:
    enabled: true
    on_startup: true
    interval: 300  # seconds

  commit:
    author_name: Dagu
    author_email: dagu@localhost

Defaults applied when git_sync.enabled: true:

FieldDefault
branchmain
push_enabledtrue
auth.typetoken
auto_sync.on_startuptrue
auto_sync.interval300
commit.author_nameDagu
commit.author_emaildagu@localhost

Authentication

HTTPS token

yaml
git_sync:
  repository: github.com/your-org/dags
  branch: main
  auth:
    type: token
    token: ${GITHUB_TOKEN}
bash
export DAGU_GITSYNC_AUTH_TYPE=token
export DAGU_GITSYNC_AUTH_TOKEN=ghp_xxxxxxxxxxxx

SSH key

Use SSH repository format, for example git@github.com:org/repo.git.

yaml
git_sync:
  repository: git@github.com:your-org/dags.git
  branch: main
  auth:
    type: ssh
    ssh_key_path: /home/user/.ssh/id_ed25519
    ssh_passphrase: ${SSH_PASSPHRASE}
bash
export DAGU_GITSYNC_AUTH_TYPE=ssh
export DAGU_GITSYNC_AUTH_SSH_KEY_PATH=/home/user/.ssh/id_ed25519
export DAGU_GITSYNC_AUTH_SSH_PASSPHRASE=your-passphrase

Environment Variables

All env vars use the DAGU_GITSYNC_ prefix.

Environment variableConfig keyDefault
DAGU_GITSYNC_ENABLEDgit_sync.enabledfalse
DAGU_GITSYNC_REPOSITORYgit_sync.repository
DAGU_GITSYNC_BRANCHgit_sync.branchmain
DAGU_GITSYNC_PATHgit_sync.path""
DAGU_GITSYNC_PUSH_ENABLEDgit_sync.push_enabledtrue
DAGU_GITSYNC_AUTH_TYPEgit_sync.auth.typetoken
DAGU_GITSYNC_AUTH_TOKENgit_sync.auth.token
DAGU_GITSYNC_AUTH_SSH_KEY_PATHgit_sync.auth.ssh_key_path
DAGU_GITSYNC_AUTH_SSH_PASSPHRASEgit_sync.auth.ssh_passphrase
DAGU_GITSYNC_AUTOSYNC_ENABLEDgit_sync.auto_sync.enabledfalse
DAGU_GITSYNC_AUTOSYNC_ON_STARTUPgit_sync.auto_sync.on_startuptrue
DAGU_GITSYNC_AUTOSYNC_INTERVALgit_sync.auto_sync.interval300
DAGU_GITSYNC_COMMIT_AUTHOR_NAMEgit_sync.commit.author_nameDagu
DAGU_GITSYNC_COMMIT_AUTHOR_EMAILgit_sync.commit.author_emaildagu@localhost

Status Values

StatusMeaning
syncedLocal content matches last synced content
modifiedLocal content differs from lastSyncedHash
untrackedLocal item exists but has no synced baseline
conflictLocal item is modified and remote changed since last sync
missingPreviously tracked file no longer exists on local disk

When an item transitions to missing, the previous status is recorded in previousStatus and the detection time in missingAt.

CLI

sync status

bash
dagu sync status

Shows repository URL, branch, last sync info, status counts, and a table of non-synced items.

sync pull

bash
dagu sync pull

Fetches and applies changes from the remote repository.

sync publish

bash
dagu sync publish my-dag -m "Update dag"
dagu sync publish memory/MEMORY -m "Update global memory"
dagu sync publish --all -m "Batch update"
dagu sync publish my-dag --force -m "Overwrite remote"
FlagDescription
-m, --messageCommit message
--allPublish all modified and untracked items
-f, --forceForce publish even with conflicts

Provide either an item ID or --all, not both.

sync discard

bash
dagu sync discard my-dag
dagu sync discard memory/dags/my-dag/MEMORY
dagu sync discard my-dag -y
FlagDescription
-y, --yesSkip confirmation prompt

Discards local changes and restores the remote version.

sync forget

bash
dagu sync forget missing-dag
dagu sync forget item-a item-b item-c
dagu sync forget missing-dag -y
FlagDescription
-y, --yesSkip confirmation prompt

Removes state entries for missing, untracked, or conflict items. Does not touch files on disk or remote. Rejects synced and modified items. Accepts multiple item IDs.

sync cleanup

bash
dagu sync cleanup
dagu sync cleanup --dry-run
dagu sync cleanup -y
FlagDescription
--dry-runShow what would be cleaned without making changes
-y, --yesSkip confirmation prompt

Removes all missing entries from sync state. Does not touch files on disk or remote.

sync delete

bash
# Delete a single item
dagu sync delete my-dag -m "Remove old dag"

# Delete with force (required for modified items)
dagu sync delete my-dag --force -m "Remove despite modifications"

# Delete all missing items
dagu sync delete --all-missing -m "Clean up missing"

# Dry run
dagu sync delete my-dag --dry-run
dagu sync delete --all-missing --dry-run
FlagDescription
-m, --messageCommit message
--forceForce delete even with local modifications
--all-missingDelete all missing items instead of a single item
--dry-runShow what would be deleted without making changes
-y, --yesSkip confirmation prompt

Deletes items from the remote repository (git rm + commit + push), local disk, and sync state. Provide either an item ID or --all-missing, not both. Untracked items cannot be deleted — use forget instead.

sync mv

bash
# Rename a DAG
dagu sync mv old-dag new-dag -m "Rename workflow"

# Force move (required for conflicting items)
dagu sync mv old-dag new-dag --force -m "Move despite conflict"

# Dry run
dagu sync mv old-dag new-dag --dry-run
FlagDescription
-m, --messageCommit message
--forceForce move even with conflicts
--dry-runShow what would be moved without making changes
-y, --yesSkip confirmation prompt

Atomically renames an item across local filesystem, remote repository, and sync state. Both source and destination must be of the same kind (e.g., both DAGs or both memory files).

Two modes:

  • Preemptive: source file exists on disk. Reads it, writes to new location, stages removal+addition in repo, commits and pushes.
  • Retroactive: source is missing but the new file already exists at destination. Reads new file, stages old removal + new addition, commits and pushes.

REST API

Endpoints

MethodEndpointDescription
GET/api/v1/sync/statusOverall sync status and item list
POST/api/v1/sync/pullPull from remote
POST/api/v1/sync/publish-allPublish selected or all modified/untracked items
POST/api/v1/sync/test-connectionTest repository and auth access
GET/api/v1/sync/configGet sync configuration
PUT/api/v1/sync/configUpdate sync configuration
GET/api/v1/sync/items/{itemId}/diffGet local vs remote diff
POST/api/v1/sync/items/{itemId}/publishPublish one item
POST/api/v1/sync/items/{itemId}/discardDiscard local changes
POST/api/v1/sync/items/{itemId}/forgetRemove state entry
POST/api/v1/sync/items/{itemId}/deleteDelete from remote + local + state
POST/api/v1/sync/items/{itemId}/moveRename across local/remote/state
POST/api/v1/sync/delete-missingDelete all missing items
POST/api/v1/sync/cleanupRemove all missing entries from state

itemId is a path parameter. If the ID contains /, URL-encode it (e.g., memory/MEMORYmemory%2FMEMORY).

Get Status

bash
curl "http://localhost:8080/api/v1/sync/status"

Response:

json
{
  "enabled": true,
  "summary": "pending",
  "items": [
    {
      "itemId": "my-dag",
      "filePath": "my-dag.yaml",
      "displayName": "my-dag.yaml",
      "kind": "dag",
      "status": "modified"
    },
    {
      "itemId": "memory/MEMORY",
      "filePath": "memory/MEMORY.md",
      "displayName": "memory/MEMORY.md",
      "kind": "memory",
      "status": "untracked"
    }
  ],
  "counts": {
    "synced": 10,
    "modified": 1,
    "untracked": 1,
    "conflict": 0,
    "missing": 0
  }
}
  • items are sorted by filePath.
  • For kind=dag, filePath is itemId + ".yaml".
  • For kind=memory, kind=skill, or kind=soul, filePath is itemId + ".md".
  • displayName equals filePath.

Diff

bash
curl "http://localhost:8080/api/v1/sync/items/memory%2FMEMORY/diff"

Publish One Item

bash
curl -X POST "http://localhost:8080/api/v1/sync/items/memory%2FMEMORY/publish" \
  -H "Content-Type: application/json" \
  -d '{"message":"Update global memory","force":false}'

Returns 409 with SyncConflictResponse when a conflict is detected and force is false.

Publish Selected Items

bash
curl -X POST "http://localhost:8080/api/v1/sync/publish-all" \
  -H "Content-Type: application/json" \
  -d '{
    "message":"Batch publish",
    "itemIds":["my-dag","memory/MEMORY"]
  }'

Omit itemIds to publish all modified/untracked items.

Discard One Item

bash
curl -X POST "http://localhost:8080/api/v1/sync/items/memory%2FMEMORY/discard"

Forget One Item

bash
curl -X POST "http://localhost:8080/api/v1/sync/items/missing-dag/forget"

Returns 400 if the item is synced or modified. Returns 404 if the item does not exist in state.

Delete One Item

bash
curl -X POST "http://localhost:8080/api/v1/sync/items/my-dag/delete" \
  -H "Content-Type: application/json" \
  -d '{"message":"Remove old DAG","force":false}'
FieldTypeDescription
messagestringCommit message
forcebooleanForce delete when item has local modifications

Returns 400 for untracked items (use forget instead) or modified items without force. Returns 404 if not found.

Delete All Missing Items

bash
curl -X POST "http://localhost:8080/api/v1/sync/delete-missing" \
  -H "Content-Type: application/json" \
  -d '{"message":"Clean up missing items"}'

Request body is optional. Response:

json
{
  "deleted": ["missing-a", "missing-b"],
  "message": "Deleted 2 missing item(s)"
}

Returns 400 when push is disabled.

Move One Item

bash
curl -X POST "http://localhost:8080/api/v1/sync/items/old-dag/move" \
  -H "Content-Type: application/json" \
  -d '{"newItemId":"new-dag","message":"Rename workflow","force":false}'
FieldTypeRequiredDescription
newItemIdstringyesNew item ID to rename to
messagestringnoCommit message
forcebooleannoForce move even with conflicts

Returns 400 for validation errors (e.g., cross-kind moves) or when push is disabled. Returns 404 if not found. Returns 409 with conflict details when a conflict is detected and force is false.

Cleanup

bash
curl -X POST "http://localhost:8080/api/v1/sync/cleanup"

Response:

json
{
  "forgotten": ["missing-a", "missing-b"],
  "message": "Cleaned up 2 item(s)"
}

Permissions

EndpointPermission
GET /sync/status, GET /sync/config, POST /sync/test-connectionNo write permission required. Returns non-error responses even when sync is not configured.
GET /sync/items/{itemId}/diffRequires sync service to be configured.
POST /sync/pull, POST /sync/publish-all, all POST /sync/items/{itemId}/* endpoints, POST /sync/delete-missing, POST /sync/cleanupRequires permissions.write_dags. Authenticated users must satisfy Role.CanWrite() (admin or manager).
PUT /sync/configAdmin only.

Write operations are blocked when Git Sync is read-only (git_sync.enabled=true and push_enabled=false).

Data On Disk

PathPurpose
{dags_dir}/Local DAG and agent files
{data_dir}/gitsync/state.jsonSync state
{data_dir}/gitsync/repo/Local Git checkout cache

state.json structure

Top-level fields:

  • version (int)
  • repository (string)
  • branch (string)
  • lastSyncAt (datetime)
  • lastSyncCommit (string)
  • lastSyncStatus (string)
  • lastError (string)
  • dags (map of itemId → DAGState)

DAGState fields:

FieldTypeDescription
statusstringsynced, modified, untracked, conflict, or missing
kindstringdag, memory, skill, or soul
baseCommitstringCommit hash when item was last synced
lastSyncedHashstringContent hash at last sync (sha256:...)
lastSyncedAtdatetimeWhen item was last synced
modifiedAtdatetimeWhen local modification was detected
localHashstringCurrent local content hash
remoteCommitstringRemote commit hash (populated on conflict)
remoteAuthorstringAuthor of the remote commit (populated on conflict)
remoteMessagestringMessage of the remote commit (populated on conflict)
conflictDetectedAtdatetimeWhen the conflict was detected
previousStatusstringStatus before transitioning to missing
missingAtdatetimeWhen the file was first detected as missing
lastStatModTimedatetimeCached file modification time (for change detection)
lastStatSizeint64Cached file size (for change detection)

Released under the MIT License.