···11+# Push hooks
22+33+`objgitd` can run a script after a successful push. The script lives **inside the
44+repository** at `.objgit/hooks/receive-pack` and runs in a restricted, in-process
55+shell — not a real operating-system shell. This document describes how to enable
66+hooks, what a hook can and cannot do, and how to write one.
77+88+## Enabling hooks
99+1010+Hooks are off by default. Start the server with `-allow-hooks`:
1111+1212+```text
1313+./objgitd -bucket $BUCKET -allow-push -allow-hooks
1414+```
1515+1616+| Flag | Env | Default | Meaning |
1717+| --------------- | -------------- | ------- | -------------------------------------------------------- |
1818+| `-allow-hooks` | `ALLOW_HOOKS` | `false` | Run `.objgit/hooks/receive-pack` after a successful push |
1919+| `-hook-timeout` | `HOOK_TIMEOUT` | `60s` | Wall-clock limit for a single hook run |
2020+2121+`-allow-hooks` is independent of `-allow-push`, but a hook can only fire on a
2222+push, so in practice you want both.
2323+2424+## When a hook runs
2525+2626+- **On push only.** The hook is named after the git service that triggered it,
2727+ and the only service that runs a hook is `receive-pack` (push). Fetches,
2828+ clones, and archives never run hooks.
2929+- **After the push completes.** Refs are already updated and the client has
3030+ already received its response when the hook starts. A hook therefore **cannot
3131+ reject a push** and its output is never shown to the person pushing — it goes
3232+ to the server log only. This is a post-receive hook, not a pre-receive gate.
3333+- **Once per changed branch.** If a push updates or creates several branches,
3434+ the hook runs once for each, with environment variables describing that
3535+ branch (see below). Branch **deletions** are skipped (there is nothing to
3636+ check out).
3737+- **Asynchronously.** The push returns immediately; the hook runs in the
3838+ background. On server shutdown, in-flight hooks are given a short grace period
3939+ to finish.
4040+4141+The script is read from the **commit that was just pushed**, so a hook travels
4242+with the branch — different branches can carry different hooks, and updating a
4343+hook is just another commit.
4444+4545+## The execution environment
4646+4747+Hooks run in [kefka](https://xeiaso.net/blog/2026/dancing-mad-sandboxing/), a
4848+virtual `bash` interpreter. **This is not a container, VM, or OS sandbox.** It is
4949+safe because of what it _cannot_ reach, not because of kernel isolation:
5050+5151+- **No system binaries.** Only kefka's built-in commands exist — roughly the
5252+ POSIX coreutils: `cat`, `ls`, `echo`, `printf`, `head`, `tail`, `cut`, `sort`,
5353+ `uniq`, `wc`, `tr`, `grep`, `sha256sum`, `base64`, `mkdir`, `cp`, `mv`, `rm`,
5454+ `touch`, `date`, `sleep`, `seq`, `expr`, and so on. There is no `git`, no
5555+ package manager, no compiler, no `curl`.
5656+- **No network.**
5757+- **No host filesystem.** The only files a hook can see are the two mounts
5858+ below.
5959+6060+Standard `bash` syntax works: variables, `if`/`for`/`while`, pipes,
6161+redirections, command substitution, `[ ... ]` tests, and `&&`/`||`.
6262+6363+### Filesystem layout
6464+6565+| Path | Contents | Writable? |
6666+| ------ | --------------------------------------------------------------- | ------------------ |
6767+| `/src` | The pushed commit, checked out. The shell starts here (`$PWD`). | **No** — read-only |
6868+| `/tmp` | Empty scratch space. Also `$HOME` and `$TMPDIR`. | Yes |
6969+7070+`/src` is a live, lazy view of the git tree: files are fetched from object
7171+storage as they are opened, so nothing is copied to disk up front. Everything
7272+under `/src` is **read-only**. Writing scratch data — including shell
7373+redirections like `echo x > out` — must target `/tmp`.
7474+7575+> **Important:** a redirection into a read-only path _aborts the script_. For
7676+> example `echo hi > /src/note.txt` does not merely fail that one line; it stops
7777+> the hook with a "read-only filesystem" error. Always redirect into `/tmp`.
7878+7979+### Environment variables
8080+8181+Each run gets variables describing the branch that triggered it:
8282+8383+| Variable | Example | Notes |
8484+| ---------------- | ----------------- | --------------------------------------------------- |
8585+| `OBJGIT_REPO` | `/myproject.git` | Repository path |
8686+| `OBJGIT_SERVICE` | `receive-pack` | Always `receive-pack` |
8787+| `OBJGIT_REF` | `refs/heads/main` | Full ref name |
8888+| `OBJGIT_BRANCH` | `main` | Short branch name |
8989+| `OBJGIT_OLD_SHA` | `0000…0000` | Previous tip; all zeros when the branch was created |
9090+| `OBJGIT_NEW_SHA` | `f43417…` | New tip |
9191+9292+For compatibility with scripts written for stock git, the same information is
9393+also fed on **stdin** as a single `<old> <new> <ref>` line.
9494+9595+## Writing a hook
9696+9797+Put the script at `.objgit/hooks/receive-pack` in your repository and commit it.
9898+The executable bit is not required — `objgitd` reads the file's contents, not its
9999+mode.
100100+101101+```bash
102102+#!/usr/bin/env bash
103103+# .objgit/hooks/receive-pack
104104+105105+echo "push to ${OBJGIT_REPO} ${OBJGIT_REF}: ${OBJGIT_OLD_SHA} -> ${OBJGIT_NEW_SHA}"
106106+107107+# /src is the checkout of the new commit; the shell starts there.
108108+echo "top-level contents:"
109109+ls /src
110110+111111+# Read a file out of the push.
112112+if [ -f /src/go.mod ]; then
113113+ module="$(head -n 1 /src/go.mod | cut -d' ' -f2)"
114114+ echo "go module: ${module}"
115115+fi
116116+117117+# Scratch work goes in /tmp.
118118+manifest=/tmp/manifest.txt
119119+echo "ref ${OBJGIT_REF}" > "${manifest}"
120120+echo "sha ${OBJGIT_NEW_SHA}" >> "${manifest}"
121121+cat "${manifest}"
122122+123123+echo "hook done"
124124+```
125125+126126+A copy of this example lives at
127127+[`.objgit/hooks/receive-pack`](../../.objgit/hooks/receive-pack) in this
128128+repository.
129129+130130+## Observing hooks
131131+132132+All hook activity is logged through the server's structured (`slog`) logger:
133133+134134+- `hook: running` — a hook started, with `repo`, `service`, `ref`, and `sha`.
135135+- `hook: finished` — success, with the hook's `exit` code, captured `stdout`,
136136+ and `stderr`.
137137+- `hook: finished with errors` — the hook exited non-zero, failed to parse, hit
138138+ the timeout, or tried to write somewhere read-only. The error is attached
139139+ under the `err` key.
140140+- `hook: no hook file in pushed tree` (debug level) — the push had no
141141+ `.objgit/hooks/receive-pack`, so nothing ran.
142142+143143+Because output is log-only, a hook cannot communicate back to the client that
144144+pushed.
145145+146146+## Limitations
147147+148148+- No writable working tree: `/src` is strictly read-only and `/tmp` is the only
149149+ scratch space.
150150+- No way to reject a push from a hook (it runs after the fact).
151151+- No system tooling, network, or arbitrary executables — only kefka built-ins.
152152+- Output is not relayed to the pusher.