pullgate lets a Linux host pull a git repo by itself and apply one exact commit only after its owner taps Apply on Telegram. Read DESIGN.md and THREAT-MODEL.md before you change code. They are the rules the code must follow.
cmd/pullgate/- the one binary. Subcommands:run,listen,apply,status,version.internal/- one package per part. Packages do not importcmd/.internal/config/- the instance and listener configs, and the checks of the files they name (DESIGN.md sections 4 and 5).internal/safedir/- the check that no other user can write a directory (rule 6, DESIGN.md section 6).internal/gitx/- git, run safely: fetch, ancestry, log, diff, and the export of a tree without filters (DESIGN.md section 6).internal/state/- the state file, the lock and the pause file.internal/spool/- the outbox and inbox files between root and the listener (DESIGN.md section 8).internal/telegram/- a small Bot API client. The token never shows in an error.internal/listener/-pullgate listen(DESIGN.md section 9).internal/message/- the text of the messages: cleaning, the watch list, the cut to one message (DESIGN.md section 12).internal/gate/-run,applyandstatus(DESIGN.md sections 7, 9, 10, 13).
contrib/- example systemd units and example configs.test/e2e/- the end-to-end test. Build tage2e.docs/releasing.md- how releases are made and checked. Only the owner merges a release pull request (chore(main): release …)..claude/agents/security-reviewer.md- the security review every change gets.
go test -race ./...
go vet ./...
golangci-lint run --build-tags e2e
go test -race -tags e2e ./test/e2e/... # needs git- This repository is public. Never add a real host name, domain name,
IP address, VPN or tailnet name, token, key, user ID, private name of a
person, household or company, or a link to a private repository. Use
example names:
example-host,config-repo,git.example.com, and IP addresses from192.0.2.0/24. Test keys and tokens are made by the test. - Never log, print or return a secret value (the bot token, the deploy key, a one-time code), not even in an error or a test failure.
- Fail closed. Every error, timeout, cancel or unclear case means: do not apply, do not approve.
- Root trusts nothing from the listener, the repo, or the commit except a code that matches its own state. It takes the SHA from its own state.
- Every change has tests, and they test the deny paths, not only the happy path.
- Every change gets a security review by the agent in
.claude/agents/security-reviewer.mdbefore it is committed. Fix what it finds. If you do not fix a finding, say why in the pull request. - If a change alters what a message shows, what root or the listener
checks, what runs as which user, or where a file goes, update DESIGN.md
and THREAT-MODEL.md in the same pull request. If it changes the config, a
unit or a command, update the README and
contrib/too. - Dependencies: the standard library and
github.com/BurntSushi/toml. Anything else needs a reason in the pull request. - Git: one feature branch and one pull request per change. Pull request
titles follow Conventional Commits
(
feat:,fix:,docs:,test:,ci:,chore:). Pull requests are squash merged. Never push tomaindirectly. - Open work is a GitHub issue with a "Done when" checklist, never a TODO file.
- Every error says how to fix it: what is wrong, where (the file and key, or the step), and how to fix it.
- Docs use plain English and short sentences. Many readers are not
native speakers. Dates are absolute (
2026-11-11).