Git Sync
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
type: custom:git-sync-cardInstall
- 1Open the repository in the Marketplace with the Add to Marketplace button above, or add
github.com/juju35/ha-git-syncas a custom repository (category Integration). - 2Download it from the Marketplace.
- 3Restart Home Assistant and add the integration from Settings → Devices & services.
Gallery 10
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.gitignorethat does not excludesecrets.yamland.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_taskentity (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-keyscanneeded - A failed push stays visible on the status until a later push succeeds, and is retried at the next commit
git_sync_committedandgit_sync_exportedevents- 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_diffaction - 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_taskintegration, 2025.8 or newer)gitandsshin the Home Assistant container: they are in the official imagesThe 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 mainWithout
push -u, Git Sync sets the upstream at its first push (0.1.2 or newer).A
.gitignorekeeping 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
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#
- Developer tools → Actions →
git_sync.generate_ssh_key→ Perform action: the response holds the public key (an existing key is kept) - 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_lineGit status sensor, the commit script and the commit and dashboard export automations - the
input_buttonandinput_texthelpers used for the manual push and the last push or export time, and thegit_push.shscript
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:
- 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;
- checks the changes for sensitive files, on a copy of the Git index;
- 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; - 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; - 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
Changelog
All 21 versions 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.installaction 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/mediawould 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_exporton 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.js118 KB → 68 KB) and each other language is its own<file>.i18n.<lang>.jsfile 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.mjsbefore 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,passwdandpinkeys, of YAML list items (- password: …), and anyBearertoken. - 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.