Skip to main content
Trinity
Guides/GitHub Sync

GitHub Sync

Keep agents in sync with GitHub (or self-hosted Git) repositories using two modes: Source mode (pull-only, default) and Working Branch mode (bidirectional).

Why Every AI Agent Needs a GitHub Repo

Apr 2026

Concepts

•Source Mode (default) — Pull-only. The agent pulls from the repo but never pushes. Used for deploying agent code from a canonical source.
•Working Branch Mode — Bidirectional. The agent has its own branch (e.g. trinity/<agent>/<id>) and can push changes back. Used for agents that modify their own code.
•Branch Selection — Specify a branch via URL syntax github:owner/repo@branch during creation, or via the source_branch parameter in MCP.
•Branch Owner — Each working branch is owned by a single agent instance. Ownership is enforced at the database layer to prevent concurrent pushes from clobbering each other.
•Parallel History — The agent's branch and its upstream have no shared commit ancestor — each side evolved independently. Requires an explicit resolution choice.

Creating an Agent with Sync

Agents created from a Git template automatically get sync configured. The default mode is Source (pull-only).

How git authenticates inside the agent

The agent's origin remote carries no token. On every fetch, pull, and push, git asks Trinity's credential helper for the agent's GitHub token, so the token stays out of .git/config and out of the container's process list and logs. When you set or rotate a token, it is written to the agent's workspace .env, which the helper reads first, so the next git operation uses it without a restart. On a self-hosted git server the helper answers only for TRINITY_GIT_BASE_URL. For what the agent can still read, and what upgrading does to existing agents, see GitHub PAT Setup → How git gets the token.

Using Sync in the UI

1

Open the agent detail page to see Git status (branch, last sync, pending changes, ahead/behind counts).

2

Click Pull to fetch the latest commits from the remote.

3

Click Sync to run a full sync operation (pull-only in Source mode; pull + push in Working Branch mode).

4

View the git log to inspect recent commits.

What a Push Does to .gitignore

Before every push, Trinity rebuilds the agent's .gitignore around its own rules and then untracks anything the rules now cover:

# >>> Trinity default ignore rules — managed; your own rules go BELOW and win >>>
   (caches, virtualenvs, local databases, generated content, …)
# <<< Trinity default ignore rules <<<
   (the agent's own rules, in their original order)
# >>> Trinity protected rules — managed; NOT overridable >>>
   (credential files and the agent's .trinity/ state)
# <<< Trinity protected rules <<<

The order is the point. Git is last-match-wins, so the managed defaults sit above the agent's own rules and a !negation the agent wrote keeps winning — a file the agent chose to keep is never silently untracked by the platform's defaults. The protected floor at the bottom cannot be overridden, so a stray rule can never re-track a credential file or drop the agent's .trinity/ hooks. The rebuild is idempotent: an unchanged file is left untouched, so the auto-sync loop has nothing to re-commit.

A Push then reports what the sweep changed. The sync response and the git_sync MCP result carry removed_paths (tracked → untracked by this push), unignored_paths (newly un-ignored and committed by this same push), and shadowed_negations (a !rule of yours that a managed pattern still overrides — the deciding pattern is named); the Git tab's toast and the commit message state the untracked and un-ignored counts and paths. When a push actually changed what is tracked, Trinity also files an operator-queue notice — Push untracked files that now match .gitignore, Push committed files that were previously gitignored, or Push changed which files are tracked (.gitignore sweep) — naming the paths, so an unattended scheduled sync cannot untrack files for weeks without anyone noticing. Find it on the Operations page. A newly un-ignored path that was a secret is already in the remote's history: rotate it and remove the rule.

One limit, reported rather than fixed: many default patterns are directory-form (node_modules/, content/), and git never descends into an excluded directory, so a negation beneath one is inert wherever it sits. Such rules appear under shadowed_negations.

Sync health polling

Trinity reads the git status of every git-enabled agent once a minute. Each read runs a git fetch inside the agent, so on a large fleet you can poll less often by setting SYNC_HEALTH_POLL_INTERVAL_SECONDS in the backend's .env (default 60; see Single-Server Deployment → .env reference). When an agent's consecutive sync failures reach three, an alert lands in the Operating Room.

The read is built to stay out of the agent's way:

•It takes no git lock. The status read never takes the repository's .git/index.lock, so the agent's own git add or git commit cannot fail because Trinity was looking.
•Callers share one read. The background poll, the Git tab, and the get_git_status MCP tool share a single in-flight computation instead of stacking parallel fetches. The response's computed_at says when that snapshot was taken, so a result can be up to one fetch old.
•Stuck locks are reported, not deleted. Trinity never removes a lock inside a running container, because deleting a lock that a live git process still holds can corrupt the index. When the same index.lock stays unchanged across at least three status reads spanning 15 minutes or more, the status response carries index_lock_stuck and the backend logs a warning once.
•A restart clears stale locks. At container start no git process can be running, so the startup script removes leftover git locks — including those of submodules and linked worktrees — and logs each one. The status response then records the cleanup under lock_recovery, and the backend logs it once.

Initializing Sync for Existing Agents

Agents created without a Git repository can be connected after the fact:

•Use the Git repo initialization flow in the UI.
•Via MCP: initialize_github_sync(agent_name, repo_owner, repo_name) — creates the repository if it does not exist (private by default; create_repo, private and description are optional)

Binding an agent to a repository you own

An agent created from a public template clones with no token — it works, it learns, it accumulates a workspace, but it cannot push anywhere. Bind to your own repo on the agent's Git tab is how you take ownership of it in place, without recreating the agent.

What it does, in order:

1

Creates the destination repository under your account if it doesn't exist (private by default), using a GitHub token you supply in the form.

2

Pushes the agent's current workspace history into it — not the template's, the agent's.

3

Repoints the agent's origin at the new repository.

4

Saves your token as the agent's per-agent token, so restarts never fall back to a shared platform token.

5

Rebuilds the container so the change survives a restart.

The agent keeps its name, its identity, its 180-day name reservation, its data volumes, and its history. Nothing is re-provisioned.

This is a rebind, not a fork— an agent that already has a writable repository can be rebound too, which is what you want after a typo'd destination, the wrong token account, or an org migration.

Requirements and refusals:

•The agent must be running (the operation reads its live workspace).
•Owner-only and human-only — there is deliberately no MCP tool, because binding requires putting your personal token in the request.
•Working-Branch-mode agents are refused (they need a branch re-reservation), as are agents with no Git configuration at all, including local-template agents and the system agent.
•A destination that already contains unrelated commits, or that another agent is already bound to, is refused rather than overwritten.
•A retry after a partial failure is safe. Re-submitting the same destination resumes; Bind status on the Git tab reports what the database and the live container each believe, so you can tell whether a lost response actually completed.

After binding, the agent pushes normally and appears on all git-sync surfaces.

Conflict Resolution

When sync can't complete cleanly, Trinity opens the Git Conflict Modal with a plain-English explanation and operator-readable resolution options. The modal classifies the conflict into one of six cases:

ClassWhat happenedTypical resolution
AHEAD_ONLYLocal has commits the remote doesn't; remote is unchangedPush
BEHIND_ONLYRemote has new commits; local is unchangedPull
PARALLEL_HISTORYBoth sides have commits and share no common ancestorAdopt upstream or Force Push (see below)
UNCOMMITTED_LOCALUncommitted working-tree changes block the syncCommit, stash, or discard
AUTH_FAILUREGit could not authenticate with the remoteUpdate the agent's GitHub PAT
WORKING_BRANCH_EXTERNAL_WRITESomeone else pushed to this agent's working branchAdopt upstream or Force Push

Each class renders a title, bullet-point explanation, and recommendation. Raw git stderr is hidden inside a collapsible <details> for developers.

Parallel history

Trinity detects parallel history at modal open by computing the common ancestor (git merge-base HEAD origin/<branch>). When no shared ancestor exists and the upstream is ahead, the modal replaces the standard Pull First / Force Push buttons with a clearer choice:

•Adopt latest upstream (preserve my state) — reset to the upstream tip while preserving workspace state flagged for persistence. (Preserve-state execution ships in a follow-up; the adopt primitive resets to upstream today.)
•Force Push — destructive. Overwrites the upstream with the agent's branch. Use only if you're certain no one else depends on the remote state.

Branch ownership enforcement

Working branches are ownership-locked at the database layer. If two agent instances try to push to the same branch simultaneously, the losing push fails with a "stale info — remote SHA has moved" error. Retry the sync; the winner's state is already upstream.

This is enforced silently — you only see the error if you trigger parallel syncs (e.g. UI + scheduled job at the same moment).

Self-Hosted Git

Trinity supports Gitea, GitHub Enterprise Server, and other Git hosts via two environment variables on the backend:

VariableExamplePurpose
TRINITY_GIT_BASE_URLhttps://gitea.example.comClone/push base URL
TRINITY_GIT_API_BASEhttps://gitea.example.com/api/v1REST API base for repo creation/validation

Trailing slashes are stripped automatically. Defaults target github.com and https://api.github.com— existing deployments need no changes. Agent creation and sync flows work identically regardless of the underlying host.

Prerequisites

For private repositories or push operations, you need a GitHub Personal Access Token configured in Trinity Settings. See GitHub PAT Setup for instructions.

Git Sync API

EndpointMethodDescription
/api/agents/{name}/git/statusGETGit sync status including ahead, behind, common_ancestor_sha, pull_branch, plus computed_at, lock_recovery, and index_lock_stuck (see Sync health polling)
/api/agents/{name}/git/syncPOSTTrigger sync; the response carries removed_paths, unignored_paths, and shadowed_negations from the .gitignore sweep
/api/agents/{name}/git/logGETRecent commits
/api/agents/{name}/git/pullPOSTPull from remote
/api/agents/{name}/git/configGETThe agent's stored git configuration (repo, mode, branch)
/api/agents/{name}/git/initializePOSTConnect an agent created without a repository to GitHub
/api/agents/{name}/github-patGET / PUT / DELETEPer-agent PAT override: status only, set, or clear (back to the platform PAT)
/api/agents/{name}/git/reset-to-main-preserve-statePOSTAdopt origin/main as the new baseline while keeping the agent's persisted state
/api/agents/{name}/git/auto-syncGET / PUTThe 15-minute auto-sync heartbeat for this agent
/api/agents/{name}/git/freeze-schedules-if-failingGET / PUTPause scheduled executions while sync is failing
/api/agents/{name}/git/sync-stateGETThe persisted sync-state row for this agent
/api/agents/{name}/git/bind-to-own-repoPOSTBind to a repository you own (owner-only, human-only)
/api/agents/{name}/git/bind-to-own-repo/statusGETReconcile a binding whose response was lost
/api/agents/sync-healthGETPer-agent sync health for the fleet
/api/fleet/sync-auditGETFleet sync audit, including duplicate repository bindings

MCP tools: initialize_github_sync, get_git_status, git_sync, get_git_log, git_pull, get_git_sync_state, reset_to_main_preserve_state. Mutating tools are owner-only; a shared key gets read and pull.

There is no MCP tool for binding to your own repository — it requires your personal token.

See the Backend API Docs (http://localhost:8000/docs when self-hosted) for full request/response schemas.

Limitations

•Binding to your own repository requires the agent to be running and is not available in Working Branch mode.
•A copy-imported agent has no Git configuration at all. Use Initialize GitHub Sync rather than bind-to-own-repo.
•Repository maintenance (repack/gc) runs on the agent's own home repository. Sub-repositories cloned into the workspace get no automatic maintenance.
•A stuck index.lock is reported but never removed while the agent runs. Restart the agent to clear it.

See Also

•GitHub PAT Setup — Configure a Personal Access Token before using sync
•Creating Agents — Creating agents from Git templates
•Monitoring — The Health tab and agent heartbeats
•Operating Room — Where sync-health alerts and a push's .gitignore sweep notice land
•Git remote token scrub (migrations/GIT_REMOTE_TOKEN_SCRUB_2026-09.md in the Trinity repo) — Operator runbook: what upgrading does to existing agents' remotes