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.
pushreplaces 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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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.