Skip to main content

hermes-setu

hermes-setu mirrors a local git worktree to a working tree on a remote host, where an agent can work on it and commit, and synchronises commits between the two. Remote paths are derived from the local path relative to $HOME, and the mapping is kept in a local registry, so that the daily interface is:

hermes-setu add      # in any directory of a worktree; prints the remote path
hermes-setu pull     # bring the agent's commits here
hermes-setu push     # make the remote tree match the local branch
hermes-setu delete   # remove the remote tree
hermes-setu prune    # remove remote state whose local counterpart is gone

The remote host requires only sshd, a POSIX shell and git; nothing is installed there.

Requirements

  • Local: Python ≥ 3.11, git ≥ 2.31.
  • Remote: git ≥ 2.23, reachable with non-interactive ssh (key or agent).

Installation

uv tool install git+https://<host>/<owner>/hermes-setu
# or, from a checkout
uv tool install .

Configure a host once:

hermes-setu setup coding-agent --root ~/agent-work

setup creates <root>/repos and <root>/trees on the host, records the absolute root, and makes the first configured host the default.

Path mapping

add locates the top of the enclosing worktree and its repository, and derives both remote paths from their location relative to $HOME:

Local Remote
~/src/proj (worktree) <root>/trees/src/proj
~/src/proj/.git (repository) <root>/repos/src/proj.git (hub)
~/wt/proj-feature (linked worktree of the same repository) <root>/trees/wt/proj-feature, same hub
submodule lib/inner of ~/src/proj <root>/repos/src/proj.modules/lib--inner.git

Paths outside $HOME, and $HOME itself, are rejected.

Commands

Command Effect
setup NAME [--ssh DEST] [--root DIR] Records a host and creates its root directory.
add [PATH] [--host NAME] Registers the worktree containing PATH (default: current directory), creates the hub and the remote tree for its current branch, and prints the tree path. If already registered, prints the path and changes nothing.
push [--ff-only] Makes the remote branch and tree identical to the local branch, including submodules. If a different branch is checked out locally than the remote tree follows, the remote tree switches to it.
pull [--accept-foreign] Fetches the remote branch and its submodule commits, checks them, and integrates them.
status, path, shell Comparison with the remote tree, remote tree path, interactive shell in the tree.
list All registered worktrees; entries whose local worktree is gone or replaced are marked [stale].
delete [--force] Removes the remote tree and its branch. Removes the hub, its sub-hubs and the local git remote when no tree of the repository remains. Refuses while the remote has commits not in the local branch or uncommitted changes.
prune [-n] [--host NAME] See below.

-C PATH works as in git.

Synchronisation semantics

The local branch is authoritative.

push replaces the state of the remote branch and tree with the local branch. Nothing on the remote is discarded without a reference:

Remote state before push Preserved as
Commits not contained in the local branch refs/setu/backup/<UTC time>/<branch>
Commits on a detached HEAD refs/setu/backup/<UTC time>/<branch>.head
Uncommitted and untracked files refs/setu/backup/<UTC time>/<branch>.wip (a stash commit)
Uncommitted files inside submodules the stash of that submodule in the tree

Rebase, merge, cherry-pick and revert operations in progress in the tree are quit before the update. Backups are fetched locally as agent-backup/<UTC time>/<branch>, for example:

git log agent-backup/20261027T171500Z/main
git stash apply agent-backup/20261027T171500Z/main.wip

push --ff-only refuses instead of replacing remote commits.

pull requires the locally checked-out branch to be the one the remote tree follows. It fetches the remote branch and, before integrating, the submodule commits it references. It then

  • does nothing if the local branch already contains the remote branch;
  • fast-forwards if possible;
  • otherwise merges. Conflicting submodule gitlinks are resolved to the local commit. Conflicts in files are left for manual resolution, as with git merge.

A subsequent push sends the merge result to the tree. Uncommitted changes in the tree are never transferred; pull reports their number.

Without submodules, git fetch agent and git pull agent <branch> also work.

Replaced and removed paths

Each hub records the machine it was created from (machine in the configuration, by default the host name), the local repository path and the repository's root commit. Each remote tree records the local worktree path it mirrors. A local path counts as replaced when it no longer holds a working tree of the recorded repository, as identified by its git directory and root commit.

add handles a replaced path directly: if the hub or the tree at the mirrored remote path belongs to a repository that no longer exists at its recorded local path, it is removed and recreated for the current repository.

prune removes, for every configured host (or --host):

  • registrations whose local worktree no longer exists or was replaced;
  • hubs, together with their sub-hubs and trees, whose local repository no longer exists or no longer contains the recorded root commit;
  • trees whose local worktree no longer exists or was replaced. The tree's branch is removed from the hub only if its head is contained in a local branch; otherwise the branch is kept and reported;
  • registrations whose remote tree no longer exists.

Empty parent directories under <root>/trees and <root>/repos are removed. Hubs recorded for another machine are never touched. prune -n prints the actions without performing them. Rewriting a repository's root commit (for example with git filter-repo) makes its hub count as replaced.

Commit identity

The hub and every submodule clone in the trees are configured with the commit identity from [identity] in the configuration file, falling back to the local repository's user.name and user.email. A commit-msg hook in the hub, also used by the submodule clones, removes lines matching the strip patterns, by default:

^ *Co-Authored-By:.*Claude
^ *Claude-Session:
Generated with \[Claude Code\]

The hook can be bypassed (git commit --no-verify, --author, environment variables). pull therefore checks every incoming commit: if its author or committer email differs from the configured identity, or its message contains a line matching a strip pattern, nothing is merged and the commits are listed. --accept-foreign overrides the check for one pull; enforce = false disables it. Submodule commits are not checked.

Remote layout

<root>/
  repos/<path>.git                bare hub: branches, incoming/*, backups
  repos/<path>.modules/<name>.git bare sub-hub per submodule (nested: <a>--<b>.git)
  trees/<path>                    linked worktree of a hub

The hub holds refs/heads/<branch>, which is checked out in the tree and receives the agent's commits, and refs/heads/incoming/<branch>, which receives the local branch on push. Local pushes therefore never target a checked-out branch.

Submodules

push sends each commit referenced by a gitlink, recursively, to the submodule's sub-hub as refs/setu/pinned/<sha>, then updates the tree with git submodule update --init --force against the sub-hubs. The submodule URLs in .gitmodules are not used on the remote host.

pull first copies commits that the agent made inside submodules from the tree into the sub-hubs, then fetches them into the local submodule repositories, then integrates the superproject and runs git submodule update --recursive.

Submodules must be initialised locally (git submodule update --init --recursive). A submodule that the agent adds is reported and not fetched.

Configuration

~/.config/hermes-setu/config.toml ($XDG_CONFIG_HOME and $HERMES_SETU_CONFIG are honoured):

default = "coding-agent"
# machine = "laptop"         # name recorded in hubs; default: host name

[hosts."coding-agent"]
transport = "ssh"            # or "local": a directory on this machine
ssh = "coding-agent"         # ssh destination, default: the host name
root = "/home/jayesh/agent-work"
remote_name = "agent"        # name of the git remote in local repositories
ssh_options = []             # extra arguments for ssh
# multiplex = false          # disable ssh connection sharing

[identity]
name = "Jayesh Badwaik"
email = "j.badwaik@fz-juelich.de"
enforce = true
strip = ["^ *Co-Authored-By:.*Claude", "^ *Claude-Session:", "Generated with \\[Claude Code\\]"]

add configures the git remote in the local repository:

[remote "agent"]
    url = coding-agent:/home/jayesh/agent-work/repos/src/proj.git
    fetch = +refs/heads/*:refs/remotes/agent/*
    fetch = ^refs/heads/incoming/*
    fetch = +refs/setu/backup/*:refs/remotes/agent-backup/*

Local state

Path Content
~/.local/share/hermes-setu/registry.json registered worktrees: host, repository, root commit, branch, hub and tree paths ($XDG_DATA_HOME is honoured)
~/.local/var/hermes-setu/lock lock serialising commands that change state
~/.local/var/hermes-setu/log.jsonl one record per invocation: time, directory, arguments, exit status, error
~/.local/tmp/hermes-setu/ssh-* ssh control sockets; one connection is shared by all ssh and git operations of a command and kept for 60 s

Limitations

  • Only committed state is transferred in either direction.
  • push replaces the tree while the agent may be working in it. The agent's state is preserved as backups, but its working tree is interrupted.
  • A remote tree is removed together with any directories below it, including trees of other repositories nested at deeper paths.
  • Git LFS objects are not transferred.

Development

uv sync
uv run pytest

The tests run every command against a local transport and, for the ssh code paths, against a stand-in ssh executable.

License

Apache License 2.0; see LICENSE.

Metadata

Release files for hermes-setu 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for hermes-setu 0.2.0
File Size Uploaded Provenance Actions
hermes_setu-0.2.0.tar.gz 36.1 kB Without provenance Details for hermes_setu-0.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for hermes-setu 0.2.0
File Interpreter ABI Platform Actions
hermes_setu-0.2.0-py3-none-any.whl Python 3 none any Details for hermes_setu-0.2.0-py3-none-any.whl

Total release size: 69.7 kB

Release files / hermes_setu-0.2.0.tar.gz

Download URL hermes_setu-0.2.0.tar.gz
Size 36.1 kB
Tags Source
SHA-256 checksum
How to use checksums
078442127c2e5fc33218e0e26a07e4e6766199ab2e8404a32bef389a221ef52e
BLAKE2b-256 checksum
How to use checksums
ee63889689478a39ecc99093fe60b5fbd0f3f554b17125c87625dc59225cc8c7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via Hatch/1.18.1 {"ci":null,"cpu":"x86_64","distro":{"libc":{"lib":"glibc","version":"2.44"},"name":"Arch Linux"},"implementation":{"name":"CPython","version":"3.14.8"},"installer":{"name":"hatch","version":"1.18.1"},"openssl_version":"OpenSSL 3.6.5 29 Sep 2026","python":"3.14.8","system":{"name":"Linux","release":"6.18.57-2-lts"}} HTTPX2/2.13.1

Release files / hermes_setu-0.2.0-py3-none-any.whl

Download URL hermes_setu-0.2.0-py3-none-any.whl
Size 33.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ef400a0821bb26f750ab5e23f482941122c739b3654a3946104bb9163343133c
BLAKE2b-256 checksum
How to use checksums
947e490bfc8b75ff44f048553e76f6c4c199c0f9f8949f5f997a29ce7eeeb16d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via Hatch/1.18.1 {"ci":null,"cpu":"x86_64","distro":{"libc":{"lib":"glibc","version":"2.44"},"name":"Arch Linux"},"implementation":{"name":"CPython","version":"3.14.8"},"installer":{"name":"hatch","version":"1.18.1"},"openssl_version":"OpenSSL 3.6.5 29 Sep 2026","python":"3.14.8","system":{"name":"Linux","release":"6.18.57-2-lts"}} HTTPX2/2.13.1

Security for hermes-setu 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page