· Updated

Keeping My Home Assistant Configuration Under Version Control (with AI Commit Messages)

How I set up Git inside Home Assistant to track changes, generate commit messages with AI, and maintain a clear history of my configuration

At some point I realized that my Home Assistant setup had crossed a line.

Not in size — but in importance.

I had:

  • Automations that took weeks to tune
  • YAML that I didn’t want to touch because “it works”
  • Changes I made late at night and forgot about the next day

Backups were there, sure — but backups answer only one question:

“Can I go back to some previous state?”

What I actually wanted was:

  • To know what changed
  • To know when
  • And ideally why

So I decided to put my entire Home Assistant /config directory under Git — and then automate the whole thing so I wouldn’t have to think about it ever again.

This is a log of what I ended up building, the decisions I made along the way, and the trade-offs I accepted.


Why git, and why inside Home Assistant

I didn’t want an external script, a cron job on another machine, or a CI pipeline somewhere else.

My constraints were:

  • Everything runs inside Home Assistant
  • No manual steps once set up
  • No interactive authentication
  • Safe by default (no secrets in Git)

Git is perfect for this:

  • It’s already available in the HA OS environment
  • It’s reliable and boring
  • It gives diffs, history, and rollback

The only tricky part is orchestration — and Home Assistant automations turned out to be more than capable.


Where I did everything: VS Code App

I did all setup work using the VS Code App (formerly the VS Code add-on).

That choice alone removed a lot of friction:

  • I could edit YAML and test commands in the same place
  • The integrated terminal runs directly inside Home Assistant
  • No SSH-ing around or guessing paths

From here on, every command I mention was run from the VS Code App terminal.


Initializing the repository

The first concrete step was simply turning /config into a Git repository:

cd /config
git init

Then I created a private repository on my Git provider and added it as a remote:

git remote add origin git@github.com:your-user/home-assistant-config.git

I deliberately chose SSH, not HTTPS:

  • No tokens to rotate
  • No credentials stored in plaintext
  • Better suited for automation

SSH authentication

Since Home Assistant automations can’t answer prompts, SSH had to be:

  • Passwordless
  • Non-interactive
  • Fully predictable

I generated a dedicated SSH key just for Home Assistant:

mkdir -p /config/.ssh
ssh-keygen -t ed25519 -f /config/.ssh/id_ed25519 -C "home-assistant"

No passphrase. This key exists only for Git access, nothing else.

I added the public key to my Git provider and kept the private key strictly inside /config/.ssh.


The known_hosts gotcha

The first push failed with:

Host key verification failed.

Which makes sense: SSH wanted confirmation.

Because this is an automated system, I solved it explicitly by populating known_hosts myself:

ssh-keyscan github.com >> /config/.ssh/known_hosts

That single command removed all interactivity.

⚠️ Security note This is safe for well-known providers like GitHub or GitLab. I would not do this blindly for an unknown SSH server.


What I absolutely did not want to commit

Before committing anything, I stopped and wrote a proper .gitignore.

This is not optional.

My Home Assistant config contains:

  • Runtime state
  • Cached data
  • Local secrets
  • Private keys

None of that belongs in Git.

Here’s the .gitignore I ended up with:

/.cloud
/.storage
/everything-presence-zone-configurator/fw_cache
/image
/tts
/.ha_run.lock
/.HA_VERSION
/callgrind.out.*
/home-assistant*
/profile.*.cprof
/zigbee.*
/.ssh

secrets.yaml

__pycache__
*.pyc

/custom_components

About secrets

I already followed HA best practices:

  • All sensitive values live in secrets.yaml
  • YAML files reference them via !secret

That made excluding secrets.yaml easy and safe.

Once a secret hits Git history, it’s effectively leaked — even if you delete it later. I treated this as a hard rule.


Wiring git into Home Assistant

Home Assistant’s shell_command integration is simple but powerful enough for this.

I defined a small set of Git commands:

shell_commands:
  git_add: >
    sh -c "git -C /config add ."

  git_has_staged_changes: >
    sh -c "git -C /config diff --staged --quiet || echo yes"

  git_diff_staged: >
    sh -c "git -C /config --no-pager diff --staged -U35"

  git_commit: >
    sh -c "git -C /config commit -m '{{ states('input_text.configuration_commit_message') }}' --author 'Home Assistant <homeassistant@home.sibe.st>'"

  git_push: >
    sh -c "GIT_SSH_COMMAND='ssh -i /config/.ssh/id_ed25519 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/config/.ssh/known_hosts' git -C /config push"

A couple of intentional decisions here:

  • I check explicitly whether there are staged changes → no empty commits
  • I scope SSH options only to the push command
  • I set the commit author to something recognizable
  • I never let Git prompt for anything

The AI part: generating commit messages

This was the fun part.

Instead of writing commit messages myself, I let Home Assistant’s AI Task integration summarize the staged diff.

The flow is:

  1. Get the staged diff
  2. Send it to the AI
  3. Ask for a short, clean commit message
  4. Store it in an input_text
  5. Use that value in git commit

This makes commit history:

  • Readable
  • Consistent
  • Surprisingly accurate

And if I don’t like the message? I can still edit the input_text before running the automation manually.


The automation itself

At the end, everything is glued together by a single automation.

It runs:

  • Once per night (for passive safety)
  • Or manually via a button (when I want control)

It:

  • Stages changes
  • Exits early if nothing changed
  • Generates a commit message
  • Commits
  • Pushes
  • Logs success or failure
alias: Auto-sync configuration to git
description: ""
triggers:
  - trigger: time
    at: "03:17:39"
  - trigger: state
    entity_id:
      - input_button.create_a_commit_and_push
    from: null
    to: null
conditions: []
actions:
  - action: shell_command.git_add
    metadata: {}
    data: {}
  - action: shell_command.git_has_staged_changes
    response_variable: staged_check
  - condition: template
    value_template: "{{ staged_check.stdout == 'yes' }}"
  - action: shell_command.git_diff_staged
    metadata: {}
    data: {}
    response_variable: diff_result
  - action: ai_task.generate_data
    metadata: {}
    data:
      task_name: Generate git commit message
      instructions: |-
        Write a concise, clear git commit message for the diff below.

        ```diff
        {{ diff_result.stdout }}
        ```
      entity_id: ai_task.openai_ai_task
      structure:
        commit_message:
          description: >-
            The commit message: a short summary of all the changes being
            committed. Sentence case without any special characters like quotes.
          selector:
            text:
              multiline: false
              type: text
              multiple: false
    response_variable: llm_result
  - action: input_text.set_value
    metadata: {}
    target:
      entity_id: input_text.configuration_commit_message
    data:
      value: "{{ llm_result.data.commit_message }}"
  - stop: Just testing, not actually committing.
    enabled: false
  - action: shell_command.git_commit
    metadata: {}
    data: {}
    enabled: true
    response_variable: commit_result
  - if:
      - condition: template
        value_template: "{{ commit_result.returncode == 0 }}"
    then:
      - data:
          name: Git Commit
          message: |
            Commit succeeded: {{ commit_result.stdout.split('\n')[0] }}
        action: logbook.log
    else:
      - data:
          name: Git Commit
          message: "Commit failed: {{ commit_result.stderr }}"
        action: logbook.log
      - stop: Commit failed
        error: true
  - action: input_text.set_value
    metadata: {}
    target:
      entity_id: input_text.configuration_commit_message
    data:
      value: ""
    enabled: true
  - action: shell_command.git_push
    metadata: {}
    data: {}
    enabled: true
    response_variable: push_result
  - if:
      - condition: template
        value_template: "{{ push_result.returncode == 0 }}"
    then:
      - action: logbook.log
        data:
          name: Git Push
          message: "Push succeeded: {{ push_result.stdout }}"
    else:
      - action: logbook.log
        data:
          name: Git Push
          message: >-
            Push failed (code {{ push_result.returncode }}): {{
            push_result.stderr or push_result.stdout }}
      - stop: Git push failed
        error: true
mode: single

What I like most is that it fails loudly:

  • Any Git error stops execution
  • Errors are logged
  • Nothing half-succeeds silently

Why I’m happy with this setup

This gave me:

  • A full audit log of my Home Assistant evolution
  • The ability to answer “what changed?” in seconds
  • Confidence to experiment more freely
  • A safety net that’s incremental, not binary

Backups are still there — but Git is now my first line of defense.

And because everything runs inside Home Assistant itself, I don’t have to maintain or remember anything else.


Final thought

This wasn’t about “using Git because developers do”.

It was about:

  • Treating my Home Assistant setup as something worth maintaining properly
  • Reducing fear around change
  • Making the system more legible to my future self

And honestly?

Once you have this, not having version history feels reckless.