Home Assistant projects
All projects

Git Sync

Integration + card · Maintenance
v1.8.0 · Nov 11, 2026 HA 2024.12.0+ Archived

Archived. Git Sync is now part of System Hub: its repository is no longer updated. Install System Hub instead, which takes over its settings (how to migrate).

Versions your Home Assistant configuration with Git, without shell commands.

Watches the configuration folder, commits and pushes the changes on a schedule with an AI-written Conventional Commits message, exports the dashboards out of .storage, and ships a card to follow and drive it all. It replaces the usual shell_command, command_line sensor, script and automation stack.

Highlights

  • Work tree status, changed files, commits ahead and behind
  • Commit and push on a schedule or on demand
  • Remote fetched on a schedule, behind and diverged states
  • Pull before push, a diverged branch or a rebase conflict never pushed
  • Pull button and action bringing in the remote commits whenever the remote has new ones
  • Secret guard refusing secrets.yaml, .storage, keys and logs, secret values hidden from the AI
  • Repair adding secrets.yaml and .storage to .gitignore
  • AI commit messages through any AI task entity, with a safe fallback
  • Dashboards exported as JSON or readable YAML, daily or before each commit
  • SSH deploy key generated by an action, no ssh-keyscan needed
  • Diagnostics with the key path, author and credentials redacted
  • Card with the status, the last commit and push, the changed files, and a ✨ button that writes the message before committing
  • Actions in the cards for administrators only, other users seeing the status
  • Full screen diff, file by file, like a code editor
  • Commit before updates: the configuration is committed and pushed before an update installs
  • Mini card for a compact dashboard, with a detail button showing the full card

Cards

git-sync-cardgit-sync-mini-card
type: custom:git-sync-card

Install

  1. 1Open the repository in the Marketplace with the Add to Marketplace button above, or addgithub.com/juju35/ha-git-sync as a custom repository (category Integration).
  2. 2Download it from the Marketplace.
  3. 3Restart Home Assistant and add the integration from Settings → Devices & services.

Documentation

From the README

Features

Features

  • Status of the work tree: clean, changes pending, push pending, behind or diverged from the remote, or error, with the changed files, the branch and the remote. The remote is fetched every 15 minutes and before each push
  • Pull before push: when the remote has new commits, the commit is rebased on them before the push; a conflict or a diverged history is never pushed and raises a repair explaining how to resolve it
  • Secret guard: secrets.yaml, .storage/, keys, the database and logs are never committed nor sent to the AI task, secret-looking values are hidden from the AI prompt, and a repair offers to complete a .gitignore that does not exclude secrets.yaml and .storage
  • Automatic commit and push every N minutes (60 by default) when something changed, and on demand from a button, an action or the card
  • Commit before updates (option): when an update is installed (Home Assistant, an add-on, an integration), the configuration is committed and pushed first — chore: snapshot before updating Home Assistant Core to 2026.12.0 — so there is always a commit to go back to; one commit for updates installed together
  • AI commit messages: the staged diff is sent to an ai_task entity (Google, OpenAI, Ollama…), which writes a one-line Conventional Commits message in any of the integration's languages. Without AI task, or if it fails, a generic message is used
  • Notification after each real commit, with an optional image
  • Dashboard export: copies .storage/lovelace* to a folder, as raw JSON, readable YAML or both, every day at a set time and optionally right before each commit. A file is only rewritten when it changes
  • SSH deploy key created by an action; the push accepts the host key on first use, no ssh-keyscan needed
  • A failed push stays visible on the status until a later push succeeds, and is retried at the next commit
  • git_sync_committed and git_sync_exported events
  • Lovelace card bundled with the integration: status, last commit, changed files, last push and export, Commit & push and Export dashboards
  • Diff viewer: a full-screen view of the changes a commit would include, file by file like an editor's source control view, from the card or the git_sync.get_diff action
  • English, French, German, Spanish, Italian, Dutch, European Portuguese, Polish, Romanian, Danish, Swedish, Bulgarian, Russian, Ukrainian, Hungarian, Slovak, Czech, Slovenian, Latvian, Greek and Serbian (Cyrillic and Latin)

Prerequisites

Prerequisites

  • Home Assistant 2024.12 or newer (the AI commit messages need the ai_task integration, 2025.8 or newer)

  • git and ssh in the Home Assistant container: they are in the official images

  • The configuration folder is a Git repository with a remote:

    cd /config
    git init -b main
    git remote add origin git@github.com:<you>/homeassistant-config.git
    # after the .gitignore below and a first commit:
    git push -u origin main
    

    Without push -u, Git Sync sets the upstream at its first push (0.1.2 or newer).

  • A .gitignore keeping secrets and runtime files out of the repository, for example:

    secrets.yaml
    .storage/
    .cloud/
    .ssh/
    *.db*
    *.log*
    deps/
    tts/
    __pycache__/
    

Warning

Everything that is not ignored is pushed. Keep credentials in secrets.yaml (!secret) and prefer a private repository.

Configuration

Configuration

Add integration

Settings → Devices & services → Add integration → Git Sync. The repository settings can be changed later with Reconfigure, the options with Configure.

Setting Default Description
Repository path /config Folder containing .git. One entry per repository
Remote origin Remote to push to
Branch current branch Target branch on the remote
SSH private key /config/.ssh/id_ed25519 Key used for the push
Author name / email repository config Overrides user.name / user.email. Required when Git has no identity in the Home Assistant container (the git config --global of the Terminal add-on is not seen there): the form asks for it instead of every commit failing
Option Default Description
AI task entity the only ai_task entity, if there is one Writes the commit message from the diff, and shows the ✨ button of the card. Without it: chore: update configuration (n files)
Commit message language Home Assistant's, else English en, fr, de, es, sr, sr-latn, it, nl, pt, pl, ro, da, sv, bg, ru, uk, hu, sk, cs, sl, lv or el
Automatic commit interval 60 min Commit and push every N minutes when something changed, 0 to disable, up to 1440
Status refresh interval 60 s How often git status is read, from 10 to 3600
Remote check interval 15 min How often the status refresh also runs git fetch, so that commits pushed from elsewhere show as behind or diverged; 0 to disable, up to 1440. A fetch always runs before each push
Pull before push on When the remote has new commits, rebase on them (git pull --rebase --autostash) before pushing. Off: nothing is committed nor pushed while the remote is ahead, and a repair says so
Commit before updates off When the update.install action runs (from an update entity, the Settings page, an automation or Update all), commit and push the configuration first, while the update downloads. Once per 10 minutes, so a batch of updates gets one snapshot from before the first. A failed snapshot is logged and never stops the update
Allow tracked sensitive files off Commit the changes of sensitive files that Git already tracks (see Secret guard). A new sensitive file is always refused
Notification action none Sent after each commit, for example notify.mobile_app_phone. An action that does not exist is refused by the form
Notification image URL none Absolute HTTPS URL (required by iOS)
Daily dashboard export on Export the dashboards every day
Export time 08:00 Time of the daily export
Export folder dashboards_export Where the dashboards are written: inside the configuration folder (a relative path starts there) or a folder of allowlist_external_dirs, outside hidden folders such as .storage; any other folder is refused. The files an earlier export wrote there and that it no longer produces (deleted dashboard, format changed) are removed, unless you edited them since; any other file of the folder, such as your own lovelace-*.yaml, is never removed nor overwritten, even when it has the name of an export (a warning is logged and that file is skipped)
Export format JSON JSON (raw copy of .storage), YAML (dashboard configuration only, easier to read in diffs) or both
Export before each commit off Also export right before each commit

SSH deploy key#

  1. Developer tools → Actions → git_sync.generate_ssh_key → Perform action: the response holds the public key (an existing key is kept)
  2. On GitHub: repository → Settings → Deploy keys → Add deploy key, paste it and tick Allow write access

Entities#

Entity Description
sensor.git_sync_<repo>_status clean, dirty, ahead (committed, push pending, or a branch never pushed), behind (the remote has commits that are not here: the next commit pulls them first), diverged (both sides have commits: nothing is pushed until it is resolved, see Repairs) or error (last push failed; retried by the automatic commit). Attributes: branch, upstream, remote_url (without the user or token of an HTTPS remote), ahead, behind, changed_count, changed_files (first 50), last_error, ai_task
sensor.git_sync_<repo>_changed_files Number of changed files
sensor.git_sync_<repo>_last_commit Time of the HEAD commit. Attributes: message, sha, short_sha
sensor.git_sync_<repo>_last_push Time of the last push made by Git Sync. Attribute: message
sensor.git_sync_<repo>_last_export Time of the last dashboard export. Attribute: files
sensor.git_sync_<repo>_branch Current branch (diagnostic)
button.git_sync_<repo>_commit_and_push Commit and push now
button.git_sync_<repo>_export_dashboards Export the dashboards now

Action git_sync.commit_push#

Fetches the remote, stages every change, commits, rebases on the remote's new commits if any, and pushes. Without message, the AI task writes it. Refused when the changes include a sensitive file, or when the branch has diverged from the remote. With nothing to commit, pushes the commits left pending by an earlier failure. Returns whether a commit was made and whether it pushed.

action: git_sync.commit_push
data:
  message: "fix(corridor): lower the night light threshold"  # optional
response_variable: result  # {"results": [{"config_entry_id": "...", "committed": true, "pushed": true, "message": "..."}]}

Action git_sync.pull#

Fetches the remote and brings in its new commits: a fast-forward (git pull --ff-only, which Git refuses if it would overwrite a local change), or, when local commits are waiting to be pushed, those commits rebased on the remote ones (git pull --rebase --autostash, only on a clean work tree; a conflict aborts the rebase). Nothing is committed or pushed. This is what the Pull button of the card calls. Returns how many remote commits came in.

action: git_sync.pull
response_variable: result  # {"results": [{"config_entry_id": "...", "pulled": 2}]}

Action git_sync.generate_message#

Writes a commit message for the current changes with the AI task, without committing: the diff is computed on a copy of the Git index, so what you staged by hand stays as it is. Returns null when nothing changed; fails when no AI task is set in the options. This is what the ✨ button of the card calls.

action: git_sync.generate_message
response_variable: result  # {"message": "feat(lights): add the corridor night scene"}

Action git_sync.get_diff#

Returns the changes a commit would include, file by file, without committing or staging anything (same copy of the index as generate_message). Each file carries its status (added, modified, deleted, renamed, copied or mode), the old path of a rename, whether it is binary, its added and removed lines and its hunks, each line with its line number on both sides. A file keeps at most 2000 lines (truncated counts the rest) and a response at most 200 files (more_files counts the rest). This is what the diff viewer of the card calls.

action: git_sync.get_diff
response_variable: diff
# {"files": [{"path": "automations.yaml", "status": "modified", "old_path": null, "binary": false,
#             "additions": 7, "deletions": 2, "truncated": 0,
#             "hunks": [{"header": "@@ -12,9 +12,14 @@", "lines": [{"kind": "context", "old": 12, "new": 12, "text": "  triggers:"}, ...]}]}],
#  "more_files": 0, "additions": 14, "deletions": 3}

Action git_sync.export_dashboards#

action: git_sync.export_dashboards
response_variable: result  # {"results": [{"config_entry_id": "...", "files": ["lovelace", ...]}]}

Action git_sync.generate_ssh_key#

action: git_sync.generate_ssh_key
response_variable: key  # {"public_key": "ssh-ed25519 AAAA... homeassistant"}

Every action takes an optional config_entry_id to target one repository; without it, commit_push, pull and export_dashboards run on all of them, generate_message, get_diff and generate_ssh_key on the first one. The actions are reserved to administrators (the diff can show secrets.yaml): a call from another user is refused, automations and scripts are not affected.

Event git_sync_committed#

Field Description
entry_id Config entry of the repository
message Commit message
sha Commit hash
files Number of files committed

git_sync_exported gives entry_id and files (names of the exported files).

Card

Card

The integration serves the Git Sync Card and adds it to the dashboard resources (or loads it on every page when the resources are managed in YAML): no separate install. Add it from the card picker, or:

type: custom:git-sync-card
  • Header: status (up to date, changes, push pending, behind, diverged, error, working…), branch and remote repository
  • Last commit: message, short hash and how long ago
  • Changed files: collapsible list, folded again when you click elsewhere
  • Last push and Dashboard export, with the number of exported files
  • Commit & push opens a confirmation at the top of the card with a message box, as in VS Code: the placeholder names the branch, and the ✨ button inside it (shown when an AI task is set) writes the message for the current changes without committing, so you can read and edit it before Commit & push. Left empty, the AI task writes it when committing. Enter commits, Escape closes. Export dashboards runs the export right away. The result is shown in a toast
  • Pull (shown when the remote has commits that are not here, unless local changes sit on top of unpushed commits: commit first) brings in the remote commits right away, with git_sync.pull
  • The actions (Commit & push, Pull, Export dashboards, View changes, ✨) are shown to administrators only, as the actions themselves are reserved to them; other users see the status
  • View changes (the compare button beside the actions, also linked from the commit confirmation, shown when something changed) opens the diff viewer below

Diff viewer#

A full-screen view of what the next commit would include, laid out like an editor's source control view: the changed files on the left with their status letter (Added, Modified, Deleted, Renamed, Type or mode changed) and their added and removed lines, the selected file on the right hunk by hunk, with the line numbers of both sides, additions in green and deletions in red. On a phone it fills the screen and the files become a row of chips above the diff. Nothing is staged or committed by looking: the diff comes from a copy of the Git index (git_sync.get_diff). ↑ ↓ walk the files, Escape or a click outside closes, Commit & push goes straight to the commit confirmation of the card.

Every option is in the visual editor:

Option Default Description
entity found automatically The Status sensor of the repository
title Git Sync Card title
show_files true Collapsible list of the changed files
show_export true Last export and the Export dashboards button
language Home Assistant's en, fr, de, es, it, nl, pt (European Portuguese), pl, ro, da, sv, bg, ru, uk, hu, sk, cs, sl, lv, el, sr (Serbian, Cyrillic) or sr-Latn (Serbian, Latin)

Mini card#

A compact version for a 6-column slot: status, branch, last commit on one line, number of changed files, last push and Commit & push as an icon button (plus Pull when the remote has new commits; administrators only). The zoom button at the top right switches it to the full card (same options) and back; a click anywhere else brings back the compact view.

type: custom:git-sync-mini-card

In the compact view Commit & push opens the same message box (✨ button included) in place of the content. The mini card takes the options of the full card, plus:

Option Default Description
zoom true Zoom button switching to the full card; show_files and show_export apply to the full view

Migrating from shell commands

Migrating from shell commands

Once Git Sync has pushed once, remove from your configuration:

  • shell_command: git_commit_push, git_status_check, git_stage_diff, git_commit_push_ai, export_dashboards, git_generate_ssh_key, git_show_ssh_key, add_github_known_host
  • the command_line Git status sensor, the commit script and the commit and dashboard export automations
  • the input_button and input_text helpers used for the manual push and the last push or export time, and the git_push.sh script

Point Git Sync to the same key (/config/.ssh/id_ed25519) and export folder to keep your deploy key and history.

How it works

How it works

Git Sync runs the git command line in the configuration folder, never prompting (no terminal, SSH in batch mode). Every status interval it reads git status, the HEAD commit and the remote; every remote check interval it also runs git fetch (30 s timeout, skipped while a commit runs; a failed fetch is only logged), so that the behind count is current. A commit:

  1. fetches the remote, and stops if the branch has diverged (local commits not pushed and new remote commits), or is behind with Pull before push off;
  2. checks the changes for sensitive files, on a copy of the Git index;
  3. stages everything (git add -A) and checks the staged files again, so a sensitive file written in the meantime is unstaged (git reset) and refused; sends the statistics and the first 8000 characters of the diff, secret values hidden, to the AI task, keeps the first line of its answer without quotes, backticks or dollar signs (100 characters max), and commits;
  4. if the remote had new commits, runs git pull --rebase --autostash; on a conflict the rebase is aborted (git rebase --abort), the commit stays local and nothing is pushed;
  5. pushes.

Commits and exports never run at the same time. An automatic commit refused for a sensitive file, or blocked by a diverged or behind branch or a rebase conflict, is logged once as a warning and not tried again until the changed files or the commits ahead or behind change. Any other failure (remote unreachable, authentication refused) is tried again at every interval and logged as a warning once per distinct message, without a traceback. The last push and export are kept across restarts.

Secret guard#

A commit, and the ✨ message generation, are refused when the changes include secrets.yaml, a file under .storage/ or .ssh/, *.pem, *.key, id_rsa*, known_devices.yaml, home-assistant_v2.db* or *.log. A sensitive file that Git already tracks can be allowed with the Allow tracked sensitive files option; a new one never is, and deleting one from the repository is always allowed. In the diff sent to the AI task, the value of every key naming a secret (password, passwd, token, api_key, api-key, apikey, x-api-key, secret, client_secret, private_key, authorization, bearer, and pin as a whole word such as door_pin) is replaced with <redacted>, in key: value, - key: value (YAML list item), "key": "value" and key = value lines, and so is any Bearer <token>; !secret references are kept.

Repairs#

Git Sync raises these issues in Settings → System → Repairs:

Issue When Resolution
Cannot push (diverged) The branch and the remote both have commits the other lacks, or the rebase of a commit stopped on a conflict In a terminal in the repository: git pull --rebase, fix the conflicts (git add, git rebase --continue), then Commit & push. Removed by the next successful push
Cannot push (behind) The remote has new commits and Pull before push is off Turn the option on, or run git pull --rebase. Removed by the next successful push
.gitignore does not exclude secrets The repository holds a configuration.yaml and its .gitignore lets secrets.yaml or .storage/ be committed Fix shows the lines to add and appends them to .gitignore once confirmed (repairs are for administrators only). A file already tracked stays tracked: git rm -r --cached secrets.yaml .storage removes it from the repository, not from the disk

Diagnostics#

Settings → Devices & services → Git Sync → ⋮ → Download diagnostics gives the entry settings and the last status (branch, ahead/behind, changed files, last commit, push and export, last error), to attach to an issue. The SSH key path and the commit author name and email are replaced with **REDACTED**, and the user and password or token of an HTTPS remote URL are removed (the host and path are kept), in the remote URL and in the last error.

To debug:

logger:
  logs:
    custom_components.git_sync: debug

Support

Support

If you find this project useful, you can support its development:

Buy Me a Coffee

1.8.0Nov 11, 2026Latest

Deprecated

  • Git Sync is now a module of System Hub, and this repository is archived: no more updates here. Turned on in System Hub, the module takes over the settings of Git Sync. While both are set up, Git Sync raises a repair in Settings → Repairs, as it would otherwise run twice and register its cards twice: remove Git Sync and its card resource once the module is on. See Migrating (juju35/ha-system-hub#49, thanks @FredM67).

1.7.1Nov 9, 2026

Maintenance

  • No code change: released again to check the installation from the Marketplace.

1.7.0Nov 9, 2026

Features

  • Commit before updates (option, off by default): when the update.install action runs — Home Assistant, an add-on, an integration, from its update entity, the Settings page, an automation or Update all — the configuration is committed and pushed first (chore: snapshot before updating Home Assistant Core to 2026.12.0), so there is a commit to go back to if the update breaks something. Updates installed together get one snapshot, from before the first. A failed snapshot is logged and never stops the update.

1.6.1Nov 9, 2026

Security

  • The dashboard export only writes inside the configuration folder or a folder of allowlist_external_dirs, outside hidden folders such as .storage (symlinks followed). Any other folder is refused in the options, and an entry saved earlier with such a folder gets an error instead of an export: a folder such as /media would have handed the dashboards to every user of the media browser.

Changes

  • The default export folder is dashboards_export, read from the configuration folder (still /config/dashboards_export on Home Assistant OS); a relative folder is read from there instead of the process's working directory.

1.6.0Nov 9, 2026

Performance

  • Translations load on demand: in the release zip, the frontend files keep English only (git-sync-card.js 118 KB → 68 KB) and each other language is its own <file>.i18n.<lang>.js file next to them, fetched the first time a card or the panel needs it. Home Assistant loads the card on every dashboard, so each page used to download and parse all 22 languages to use one. The cards and panel are defined once the page's language has arrived (a few milliseconds), so they render translated from the start.

Maintenance

  • The repository keeps every language in the sources: the release runs scripts/split-frontend.mjs before building the zip, and the Validate workflow checks the split on a copy (every table is rebuilt from the split files and compared).

Fixes

  • Setup and reconfiguration check the target branch with git check-ref-format --branch: a name git refuses (main.., a space, a leading -) is reported on the form instead of making the first push fail after a successful setup.

1.5.1Nov 8, 2026

Security

  • The diff sent to the AI task also hides the values of authorization, bearer, api-key/apikey/x-api-key, private_key, passwd and pin keys, of YAML list items (- password: …), and any Bearer token.
  • The card and the mini card show the actions (commit, pull, export, diff, AI message) to administrators only; other users see the status.

Fixes

  • The Pull button is shown whenever the remote has new commits (also with local changes or after an error, and on a clean diverged branch), except with local changes on top of unpushed commits.
  • An automatic commit that fails to push or fetch is logged as one warning per distinct error, without a traceback, and is still retried at every interval.