objgit Git backed by object storage

Push hooks

objgitd can run a script after a successful push. The script lives inside the repository at .objgit/hooks/receive-pack and runs in a restricted, in-process shell — not a real operating-system shell. This document describes how to enable hooks, what a hook can and cannot do, and how to write one.

Enabling hooks

Hooks are off by default. Start the server with -allow-hooks:

./objgitd -bucket $BUCKET -allow-push -allow-hooks
Flag Env Default Meaning
-allow-hooks ALLOW_HOOKS false Run .objgit/hooks/receive-pack after a successful push
-hook-timeout HOOK_TIMEOUT 60s Wall-clock limit for a single hook run
-wasm-path WASM_PATH /app/wasm/bin,/usr/libexec/objgit/bin Comma-separated directories of WASI programs for hooks
-wasm-cache-dir WASM_CACHE_DIR empty Directory that keeps compiled WASI programs across restarts

-allow-hooks is independent of -allow-push, but a hook can only fire on a push, so in practice you want both.

WASI programs

Each .wasm file in a -wasm-path directory is a hook command. The command name is the file name without .wasm, so kustomize.wasm is kustomize. The image puts the programs from bin/ in /usr/libexec/objgit/bin.

-wasm-path works like PATH:

The daemon lists the directories one time, at startup. It reads and compiles a program on its first run. The compile of kustomize takes about 4 seconds, and this time counts against -hook-timeout. With -wasm-cache-dir, the daemon keeps the compiled programs on disk, and a run after a restart starts in less than a second.

To add a program to a deployment:

  1. Compile the program for WASI preview 1, for example with GOOS=wasip1 GOARCH=wasm go build.
  2. Put the .wasm file in /app/wasm/bin, for example from a volume mount.
  3. Restart objgitd.

A program sees the hook filesystem at /. It has no network. It gets these environment variables: PWD, HOME, and TMPDIR.

When a hook runs

The script is read from the commit that was just pushed, so a hook travels with the branch — different branches can carry different hooks, and updating a hook is just another commit.

The execution environment

Hooks run in kefka, a virtual bash interpreter. This is not a container, VM, or OS sandbox. It is safe because of what it cannot reach, not because of kernel isolation:

Standard bash syntax works: variables, if/for/while, pipes, redirections, command substitution, [ ... ] tests, and &&/||.

Filesystem layout

Path Contents Writable?
/src The pushed commit, checked out. The shell starts here ($PWD). No — read-only
/tmp Empty scratch space. Also $HOME and $TMPDIR. Yes

/src is a live, lazy view of the git tree: files are fetched from object storage as they are opened, so nothing is copied to disk up front. Everything under /src is read-only. Writing scratch data — including shell redirections like echo x > out — must target /tmp.

Important: a redirection into a read-only path aborts the script. For example echo hi > /src/note.txt does not merely fail that one line; it stops the hook with a "read-only filesystem" error. Always redirect into /tmp.

Environment variables

Each run gets variables describing the branch that triggered it:

Variable Example Notes
OBJGIT_REPO /myproject.git Repository path
OBJGIT_SERVICE receive-pack Always receive-pack
OBJGIT_REF refs/heads/main Full ref name
OBJGIT_BRANCH main Short branch name
OBJGIT_OLD_SHA 0000…0000 Previous tip; all zeros when the branch was created
OBJGIT_NEW_SHA f43417… New tip
OBJGIT_ADDED_FILES_JSON ["new.txt"] Added paths, as a JSON array
OBJGIT_CHANGED_FILES_JSON ["README.md"] Changed paths, as a JSON array
OBJGIT_DELETED_FILES_JSON ["old.txt"] Deleted paths, as a JSON array
OBJGIT_CHANGES_FILE /tmp/objgit-changes.json File containing all three arrays in a single JSON object

The file lists describe the net change of the branch tip across the push. If one commit adds a path and a later commit deletes it, that path does not appear in these lists. A new branch compares its tip against an empty tree. Renames appear as one added path and one deleted path. Mode-only changes appear in changed. Whitespace and newlines in paths are JSON escaped; parse the JSON rather than splitting on spaces or lines. A path with invalid UTF-8 bytes is represented as /objgit/raw-path/base64url/ followed by the unpadded base64url encoding of its raw bytes.

OBJGIT_CHANGES_FILE lives in the hook's writable /tmp filesystem and contains the same complete lists. For example:

{ "added": ["new.txt"], "changed": ["README.md"], "deleted": ["old.txt"] }

For compatibility with scripts written for stock git, the same information is also fed on stdin as a single <old> <new> <ref> line.

Writing a hook

Put the script at .objgit/hooks/receive-pack in your repository and commit it. The executable bit is not required — objgitd reads the file's contents, not its mode.

#!/usr/bin/env bash
# .objgit/hooks/receive-pack

echo "push to ${OBJGIT_REPO} ${OBJGIT_REF}: ${OBJGIT_OLD_SHA} -> ${OBJGIT_NEW_SHA}"

# /src is the checkout of the new commit; the shell starts there.
echo "top-level contents:"
ls /src

# Read a file out of the push.
if [ -f /src/go.mod ]; then
  module="$(head -n 1 /src/go.mod | cut -d' ' -f2)"
  echo "go module: ${module}"
fi

# Scratch work goes in /tmp.
manifest=/tmp/manifest.txt
echo "ref ${OBJGIT_REF}" > "${manifest}"
echo "sha ${OBJGIT_NEW_SHA}" >> "${manifest}"
cat "${manifest}"

echo "hook done"

A copy of this example lives at .objgit/hooks/receive-pack in this repository.

Exploring the sandbox

You can open the hook sandbox as an interactive shell over SSH. Use it to try commands before you put them in a hook.

  1. Start objgitd with -ssh-bind and -allow-hooks.

  2. Push the branch that you want to examine.

  3. Connect with a terminal:

    ssh -t -p 2222 git@host sh myproject.git
    
  4. To examine a branch other than the one that HEAD points to, give its name:

    ssh -t -p 2222 git@host sh myproject.git feature
    

The shell is the same as the one that a hook gets:

The shell is different from a hook in these ways:

To leave the shell, type exit or press Ctrl-D.

To open the shell, you need write access to the repository. If the server refuses the session, it shows one of these errors:

Error Cause
sh is disabled; start objgitd with -allow-hooks to enable it The server runs without -allow-hooks.
sh needs a terminal; use ssh -t The client did not request a PTY.
access denied You do not have write access.
repository "…" not found The repository does not exist.
branch "…" not found The branch does not exist.
HEAD is detached; name a branch: … HEAD is not a branch. Give a branch name.

Observing hooks

All hook activity is logged through the server's structured (slog) logger:

When sideband is unavailable, hook output is captured in the server log. Hook failure is logged, but it cannot undo the accepted push.

Limitations