Git backed by object storage because you can't stop me
git object-storage kefka
10

Configure Feed

Select the types of activity you want to include in your feed.

docs: document hooks

Signed-off-by: Xe Iaso <me@xeiaso.net>

Xe Iaso (May 28, 2026, 10:42 PM EDT) 9828517c ecd47c2c

+154
+152
docs/usage/hooks.md
··· 1 + # Push hooks 2 + 3 + `objgitd` can run a script after a successful push. The script lives **inside the 4 + repository** at `.objgit/hooks/receive-pack` and runs in a restricted, in-process 5 + shell — not a real operating-system shell. This document describes how to enable 6 + hooks, what a hook can and cannot do, and how to write one. 7 + 8 + ## Enabling hooks 9 + 10 + Hooks are off by default. Start the server with `-allow-hooks`: 11 + 12 + ```text 13 + ./objgitd -bucket $BUCKET -allow-push -allow-hooks 14 + ``` 15 + 16 + | Flag | Env | Default | Meaning | 17 + | --------------- | -------------- | ------- | -------------------------------------------------------- | 18 + | `-allow-hooks` | `ALLOW_HOOKS` | `false` | Run `.objgit/hooks/receive-pack` after a successful push | 19 + | `-hook-timeout` | `HOOK_TIMEOUT` | `60s` | Wall-clock limit for a single hook run | 20 + 21 + `-allow-hooks` is independent of `-allow-push`, but a hook can only fire on a 22 + push, so in practice you want both. 23 + 24 + ## When a hook runs 25 + 26 + - **On push only.** The hook is named after the git service that triggered it, 27 + and the only service that runs a hook is `receive-pack` (push). Fetches, 28 + clones, and archives never run hooks. 29 + - **After the push completes.** Refs are already updated and the client has 30 + already received its response when the hook starts. A hook therefore **cannot 31 + reject a push** and its output is never shown to the person pushing — it goes 32 + to the server log only. This is a post-receive hook, not a pre-receive gate. 33 + - **Once per changed branch.** If a push updates or creates several branches, 34 + the hook runs once for each, with environment variables describing that 35 + branch (see below). Branch **deletions** are skipped (there is nothing to 36 + check out). 37 + - **Asynchronously.** The push returns immediately; the hook runs in the 38 + background. On server shutdown, in-flight hooks are given a short grace period 39 + to finish. 40 + 41 + The script is read from the **commit that was just pushed**, so a hook travels 42 + with the branch — different branches can carry different hooks, and updating a 43 + hook is just another commit. 44 + 45 + ## The execution environment 46 + 47 + Hooks run in [kefka](https://xeiaso.net/blog/2026/dancing-mad-sandboxing/), a 48 + virtual `bash` interpreter. **This is not a container, VM, or OS sandbox.** It is 49 + safe because of what it _cannot_ reach, not because of kernel isolation: 50 + 51 + - **No system binaries.** Only kefka's built-in commands exist — roughly the 52 + POSIX coreutils: `cat`, `ls`, `echo`, `printf`, `head`, `tail`, `cut`, `sort`, 53 + `uniq`, `wc`, `tr`, `grep`, `sha256sum`, `base64`, `mkdir`, `cp`, `mv`, `rm`, 54 + `touch`, `date`, `sleep`, `seq`, `expr`, and so on. There is no `git`, no 55 + package manager, no compiler, no `curl`. 56 + - **No network.** 57 + - **No host filesystem.** The only files a hook can see are the two mounts 58 + below. 59 + 60 + Standard `bash` syntax works: variables, `if`/`for`/`while`, pipes, 61 + redirections, command substitution, `[ ... ]` tests, and `&&`/`||`. 62 + 63 + ### Filesystem layout 64 + 65 + | Path | Contents | Writable? | 66 + | ------ | --------------------------------------------------------------- | ------------------ | 67 + | `/src` | The pushed commit, checked out. The shell starts here (`$PWD`). | **No** — read-only | 68 + | `/tmp` | Empty scratch space. Also `$HOME` and `$TMPDIR`. | Yes | 69 + 70 + `/src` is a live, lazy view of the git tree: files are fetched from object 71 + storage as they are opened, so nothing is copied to disk up front. Everything 72 + under `/src` is **read-only**. Writing scratch data — including shell 73 + redirections like `echo x > out` — must target `/tmp`. 74 + 75 + > **Important:** a redirection into a read-only path _aborts the script_. For 76 + > example `echo hi > /src/note.txt` does not merely fail that one line; it stops 77 + > the hook with a "read-only filesystem" error. Always redirect into `/tmp`. 78 + 79 + ### Environment variables 80 + 81 + Each run gets variables describing the branch that triggered it: 82 + 83 + | Variable | Example | Notes | 84 + | ---------------- | ----------------- | --------------------------------------------------- | 85 + | `OBJGIT_REPO` | `/myproject.git` | Repository path | 86 + | `OBJGIT_SERVICE` | `receive-pack` | Always `receive-pack` | 87 + | `OBJGIT_REF` | `refs/heads/main` | Full ref name | 88 + | `OBJGIT_BRANCH` | `main` | Short branch name | 89 + | `OBJGIT_OLD_SHA` | `0000…0000` | Previous tip; all zeros when the branch was created | 90 + | `OBJGIT_NEW_SHA` | `f43417…` | New tip | 91 + 92 + For compatibility with scripts written for stock git, the same information is 93 + also fed on **stdin** as a single `<old> <new> <ref>` line. 94 + 95 + ## Writing a hook 96 + 97 + Put the script at `.objgit/hooks/receive-pack` in your repository and commit it. 98 + The executable bit is not required — `objgitd` reads the file's contents, not its 99 + mode. 100 + 101 + ```bash 102 + #!/usr/bin/env bash 103 + # .objgit/hooks/receive-pack 104 + 105 + echo "push to ${OBJGIT_REPO} ${OBJGIT_REF}: ${OBJGIT_OLD_SHA} -> ${OBJGIT_NEW_SHA}" 106 + 107 + # /src is the checkout of the new commit; the shell starts there. 108 + echo "top-level contents:" 109 + ls /src 110 + 111 + # Read a file out of the push. 112 + if [ -f /src/go.mod ]; then 113 + module="$(head -n 1 /src/go.mod | cut -d' ' -f2)" 114 + echo "go module: ${module}" 115 + fi 116 + 117 + # Scratch work goes in /tmp. 118 + manifest=/tmp/manifest.txt 119 + echo "ref ${OBJGIT_REF}" > "${manifest}" 120 + echo "sha ${OBJGIT_NEW_SHA}" >> "${manifest}" 121 + cat "${manifest}" 122 + 123 + echo "hook done" 124 + ``` 125 + 126 + A copy of this example lives at 127 + [`.objgit/hooks/receive-pack`](../../.objgit/hooks/receive-pack) in this 128 + repository. 129 + 130 + ## Observing hooks 131 + 132 + All hook activity is logged through the server's structured (`slog`) logger: 133 + 134 + - `hook: running` — a hook started, with `repo`, `service`, `ref`, and `sha`. 135 + - `hook: finished` — success, with the hook's `exit` code, captured `stdout`, 136 + and `stderr`. 137 + - `hook: finished with errors` — the hook exited non-zero, failed to parse, hit 138 + the timeout, or tried to write somewhere read-only. The error is attached 139 + under the `err` key. 140 + - `hook: no hook file in pushed tree` (debug level) — the push had no 141 + `.objgit/hooks/receive-pack`, so nothing ran. 142 + 143 + Because output is log-only, a hook cannot communicate back to the client that 144 + pushed. 145 + 146 + ## Limitations 147 + 148 + - No writable working tree: `/src` is strictly read-only and `/tmp` is the only 149 + scratch space. 150 + - No way to reject a push from a hook (it runs after the fact). 151 + - No system tooling, network, or arbitrary executables — only kefka built-ins. 152 + - Output is not relayed to the pusher.
+2
go.mod
··· 20 20 require ( 21 21 github.com/Microsoft/go-winio v0.6.2 // indirect 22 22 github.com/ProtonMail/go-crypto v1.4.1 // indirect 23 + github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect 23 24 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 // indirect 24 25 github.com/aws/aws-sdk-go-v2/config v1.32.17 // indirect 25 26 github.com/aws/aws-sdk-go-v2/credentials v1.19.16 // indirect ··· 40 41 github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c // indirect 41 42 github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect 42 43 github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 // indirect 44 + github.com/gliderlabs/ssh v0.3.8 // indirect 43 45 github.com/go-git/gcfg/v2 v2.0.2 // indirect 44 46 github.com/kevinburke/ssh_config v1.6.0 // indirect 45 47 github.com/klauspost/cpuid/v2 v2.3.0 // indirect