Install
$ agentstack add skill-cobibean-agent-local-cloud-sync-agent-local-cloud-sync Open-source listing — not yet scanned by AgentStack. Follow the source repository for install instructions.
Security review
⚠ Flagged1 finding(s); flagged for manual review. · v0.1.0 How review works →
- • Prompt-injection patterns
- • Secret / credential exfiltration
- • Dangerous shell & filesystem operations
- • Untrusted network calls
- • Known-malicious package signatures
- high Destructive filesystem operation.
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Agent local ↔ cloud sync
A ~100-line Python listener + a user-systemd unit. When you git push to your tracked branch, the host fast-forwards within seconds. No extra dependencies, no ngrok, no CI runner.
Good fit for:
- A single VPS or droplet that tracks a repo.
- Agent/bot runtimes where the workspace repo is the source of truth (e.g. a Hermes or Claude Code agent runtime, a Discord bot, a content scheduler).
- Anything where "ssh in and
git pull" is getting tedious.
Not a fit for:
- Multi-host deploys (use a CI runner with a deploy step).
- Anything that needs a build step beyond
git pull(add a post-pull hook, or just use CI).
How it works
- GitHub POSTs
pushevents tohttp://:/webhookwith anX-Hub-Signature-256header. - Listener verifies HMAC-SHA256 against a secret file. 401 on mismatch.
- On valid push to your tracked branch:
git -C pull --ff-only origin. - Everything logs to
journalctl --user -u github-webhook.
ping events and pushes to other branches return 200 with "ignored" and are logged.
Prerequisites
- A Linux host you can SSH to as a user that can run
systemctl --user. Root is fine. Non-root works withloginctl enable-linger. - Python 3.8+ (stdlib only — no
pip install). - The repo already cloned on the host with SSH or HTTPS auth that
git pullcan use non-interactively (deploy key, SSH agent, or stored credential). - The host must be reachable from the public internet on the port you pick (default 9000). If you're behind a firewall/NAT, this recipe won't work without a tunnel.
Install
# 1. Pick values
HOST=your-ssh-alias # e.g. from ~/.ssh/config
REPO_PATH=/root/DEV/my-repo # absolute path on the host
BRANCH=main
PORT=9000
# 2. Generate the HMAC secret on the host (only place it should exist in plaintext)
ssh "$HOST" "
mkdir -p /root/.config/github-webhook /root/bin /root/.config/systemd/user
python3 -c 'import secrets; print(secrets.token_hex(32))' > /root/.config/github-webhook/secret
chmod 600 /root/.config/github-webhook/secret
"
# 3. Copy the script + unit
scp github_webhook.py "$HOST":/root/bin/github_webhook.py
scp github-webhook.service "$HOST":/root/.config/systemd/user/github-webhook.service
# 4. Edit the unit's Environment= lines to match your REPO_PATH / BRANCH / PORT, then:
ssh "$HOST" "
chmod +x /root/bin/github_webhook.py
systemctl --user daemon-reload
systemctl --user enable --now github-webhook.service
"
# 5. Register the webhook with GitHub
SECRET=$(ssh "$HOST" 'cat /root/.config/github-webhook/secret')
IP=$(ssh "$HOST" 'curl -s -4 ifconfig.me')
gh api -X POST /repos/OWNER/REPO/hooks \
-f name=web -F active=true -f 'events[]=push' \
-f "config[url]=http://$IP:$PORT/webhook" \
-f 'config[content_type]=json' \
-f "config[secret]=$SECRET" \
-f 'config[insecure_ssl]=0'
If you're not a gh user, configure the webhook manually: repo → Settings → Webhooks → Add webhook. Content type application/json, set the secret, events = "Just the push event".
Verify
# Listener up?
ssh "$HOST" "curl -s http://127.0.0.1:$PORT/health" # → ok
# External reach?
curl -s -m 5 "http://$IP:$PORT/health" # → ok
# GitHub ping delivered on hook creation?
ssh "$HOST" 'journalctl --user -u github-webhook -n 20 --no-pager'
# → "ping received"
End-to-end:
git commit --allow-empty -m "test webhook" && git push
sleep 4
ssh "$HOST" "cd $REPO_PATH && git rev-parse HEAD" # matches your laptop HEAD
Day-2 ops
# Tail logs
ssh "$HOST" 'journalctl --user -u github-webhook -f'
# Restart
ssh "$HOST" 'systemctl --user restart github-webhook'
# Rotate secret
NEW=$(python3 -c 'import secrets; print(secrets.token_hex(32))')
ssh "$HOST" "echo '$NEW' > /root/.config/github-webhook/secret && chmod 600 /root/.config/github-webhook/secret && systemctl --user restart github-webhook"
HOOK_ID=$(gh api /repos/OWNER/REPO/hooks --jq '.[] | select(.config.url | contains("/webhook")) | .id')
gh api -X PATCH "/repos/OWNER/REPO/hooks/$HOOK_ID" -f "config[secret]=$NEW"
# Redeliver a past payload (repo → Settings → Webhooks → pick hook → Recent Deliveries)
Security model
- Secret is the only gate. The webhook URL is public. An attacker without the secret can only trigger 401s.
- With the secret (if it leaks), an attacker can forge push payloads and force
git pull --ff-only. That can't inject code —--ff-onlyrefuses anything except fast-forwards of what GitHub actually has — but it can be noisy. Rotate the secret if the host is compromised. - No TLS in this recipe. Payloads travel over plain HTTP. The HMAC protects integrity but not confidentiality. GitHub signs the body; there's nothing sensitive in a push event beyond ref + commit SHAs that are already public on a public repo. For private repos or extra paranoia, put the listener behind a reverse proxy (Caddy / nginx) with a Let's Encrypt cert and switch to HTTPS.
- Runs as the SSH user. The listener runs
git pullwith the same credentials the user has. If that's root,git pullruns as root. Fine for a single-tenant droplet — use a dedicated user if you want to narrow blast radius.
If the host's public IP changes
IP=$(ssh "$HOST" 'curl -s -4 ifconfig.me')
HOOK_ID=$(gh api /repos/OWNER/REPO/hooks --jq '.[] | select(.config.url | contains("/webhook")) | .id')
gh api -X PATCH "/repos/OWNER/REPO/hooks/$HOOK_ID" \
-f "config[url]=http://$IP:$PORT/webhook" \
-f 'config[content_type]=json'
For a long-lived setup, use a reserved IP or a domain.
Uninstall
# On the host
ssh "$HOST" '
systemctl --user disable --now github-webhook.service
rm /root/.config/systemd/user/github-webhook.service
rm /root/bin/github_webhook.py
rm -rf /root/.config/github-webhook
systemctl --user daemon-reload
'
# On GitHub
HOOK_ID=$(gh api /repos/OWNER/REPO/hooks --jq '.[] | select(.config.url | contains("/webhook")) | .id')
gh api -X DELETE "/repos/OWNER/REPO/hooks/$HOOK_ID"
Gotchas
- Zsh glob error on
gh api:config[url]=...needs single quotes — bare brackets trigger glob expansion. Error looks like "no matches found". - Repeated fields:
events[]=pushuses the repeated-field form — not a JSON array. pingvspush: GitHub firespingimmediately on hook creation. The listener returns 200 for it. Use this to confirm delivery without a real push.- "Everything up-to-date":
git pushonly triggers the webhook if there's a new commit. Usegit commit --allow-emptyto force a test push. - UFW: If UFW is active,
ufw allow /tcp. And don't forgetufw allow 22/tcpbefore enabling UFW or you lose SSH. - Hostname/IP in unit file: The unit doesn't care about the public IP — only GitHub does. If the host's IP changes, update the GitHub hook, not the unit.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: cobibean
- Source: cobibean/agent-local-cloud-sync
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.