Search

    MENU

    Hooks

    Markdown

    A hook is a command that Zinn runs after it saves a change. It receives the change’s activity records as JSON on stdin, so it can notify, sync or log what happened. Core runs hooks, so every program that uses the @zinn-dev/core package triggers them.

    Configure hooks

    Add a hooks object to the global settings file, ~/.zinn/settings.json, or to settings.json inside ZINN_DIR when it is set. Each key names a hook:

    			
    				
    						
    						{
    	"$schema": "https://zinn.sh/schemas/settings/v1.json",
    	"hooks": {
    		"notify-move": {
    			"command": "sh ~/.zinn/hooks/notify-move.sh",
    			"timeout": 10
    		}
    	}
    }
    					
    			
    		

    command is required. timeout is optional: any positive number of seconds, 30 by default. Other keys in a hook are rejected like any settings error.

    When hooks run

    After each saved change, once the database commit is done, every hook runs once, one after another in the order they are listed in settings. Hooks named with a whole number, such as "10", run first, in numeric order. Every hook runs for every change. So a hook is expected to read the records to decide what to do. A change that saves nothing, such as an edit that leaves every value unchanged, runs no hooks. backup, restore, migrate and reindex record no activity and currently run no hooks.

    A change can record several activity records, and a hook receives all of them in one run. Creating a relation records one for each of the two tasks. Deleting a label records the label and one for each task that lost it.

    The command runs through the platform shell (/bin/sh on macOS and Linux, cmd.exe on Windows) in the caller’s working directory and environment. The hook’s stdout and stderr both go to Zinn’s stderr, so Zinn’s stdout, including --json output, stays clean.

    Zinn waits for the hook process to exit, so a hook can background slow work with &. Redirect that child’s output, as in slow-sync >/dev/null 2>&1 &: a program that reads Zinn’s stderr through a pipe otherwise waits until the child closes it.

    Read the records

    Stdin has one JSON object per line, one for each activity record of the change, in the order recorded. Each object has the fields of an activity record. It adds task_key: the task’s key, such as APP-1, on task records, and null on project, column and label records.

    For example, moving a task records a line like this (formatted as wrapped for presentation here, but normally writtenas one line):

    			
    				
    						
    						{
    	"id": "019abcdf-1234-7000-8000-123456abcdef",
    	"created_at": 1792670000000,
    	"actor_name": "Jim",
    	"actor_email": "jm@topekastar.com",
    	"actor_kind": "human",
    	"actor_source": "git-config",
    	"actor_session_id": null,
    	"actor_thread_id": null,
    	"actor_agent": null,
    	"actor_model": null,
    	"actor_effort": null,
    	"actor_client": "zinn-cli/0.44.0",
    	"entity_type": "task",
    	"entity_id": "019abcdf-5678-7000-8000-123456abcdef",
    	"action": "move",
    	"changes": {
    		"column": [
    			{ "id": "019abcdf-0001-7000-8000-123456abcdef", "name": "Backlog" },
    			{ "id": "019abcdf-0002-7000-8000-123456abcdef", "name": "Done" }
    		]
    	},
    	"task_key": "MDR-3"
    }
    					
    			
    		

    This script reacts only to moves:

    			
    				
    						
    						#!/bin/sh
    node -e '
    for (const line of require("node:fs").readFileSync(0, "utf8").split("\n")) {
    	if (line === "") {
    		continue;
    	}
    
    	const record = JSON.parse(line);
    	if (record.action === "move") {
    		console.log(`${record.task_key} moved to ${record.changes.column[1].name}`);
    	}
    }'
    					
    			
    		

    A hook that exits without reading all of its stdin is fine.

    Failures

    A hook that exits non-zero, is ended by a signal, cannot start, or runs past its timeout prints a warning that names the hook on stderr. A hook past its timeout is stopped at once, together with the programs it started on macOS and Linux. The change stays saved and Zinn’s exit status does not change.

    Hooks that run Zinn

    Zinn sets ZINN_HOOKS=0 in every hook’s environment. A zinn command that a hook runs saves its change but runs no hooks, which prevents loops. Set ZINN_HOOKS=0 yourself to switch hooks off for a command or a shell session.

    A hook that wants its own writes to trigger hooks can remove the variable for that call, and must then guard against loops itself.

    Changes written from a hook are recorded with the actor inherited from the environment, unless the hook sets the ZINN_* actor variables. See Who am I? for them.