Hook Author's Guide

This guide walks through the process of writing and testing a Taskwarrior hook script. While this is a simple and straightforward process for developers, there are still many considerations. A hooks script will be developed, and the various concerns discussed.

Example Hook Script

As an example, we're going to create a hook script that detects tasks that refer to Taskwarrior bug numbers (ie 'TW-179') in the description, and replaces the bug number with a URL that links to the bug. Whenever the pattern tw-179 is found in a task description, it should change to https://github.com/GothenburgBitFactory/taskwarrior/issues/179.

This script will simply need to search for a pattern, and perform a replacement, for new tasks only. This will be a very simple hook script.

Choosing a Language

You can write a hook script in any language you wish. But there is more to consider:

  • Is performance an issue? It is not likely that you need to worry about performance, because the time spent adding or modifying tasks is a one-time cost. Performance would be more important if it affected a report.
  • Does your language have a readily available JSON parser? Most likely it does, but is it installed on the users machine? Are you going to be requiring that the user install additional software?
  • If you are considering a compiled language, will you ship source or binaries? Developers typically have compilers installed, but regular users do not. Shipping binaries means you'll need to provide them for different OSes and versions.

This example will be written in Python 2.6+, because Python is well known, modern, and commonly available. It has a built-in JSON parser. It is ideal for this task.

Hooks API

Read and understand the Hooks API. This is important because the hook script must comply with the API requirements. Taskwarrior is strict about compliance. Hook scripts have the ability to harm data, so they are carefully monitored.

Framework

From the API, we know that an on-add hook script will need to read a line of JSON from standard input, and emit an optionally modified line of JSON, optionally include feedback, and exit with a zero status indicating success.

To begin with, here is a compliant on-add hook script that does nothing, but does it properly. It can be the basis for any on-add script.

#!/usr/bin/env python

import sys
import json

added_task = json.loads(sys.stdin.readline())
print(json.dumps(added_task))
sys.exit(0)

This script reads a line of JSON from input and parses it. This JSON represents the task being added. The JSON is then serialized and written to output, without modification. An exit code of zero indicates that the added task is accepted.

Although this script does nothing to the task, it only requires a few more lines added to be complete.

Testing

You can test your hook script independently from Taskwarrior, which is a good idea. First we make our script executable, then we simply run it from the command line and feed it sample JSON. Here is an example test run, using valid JSON, but it is not a valid task - it's just a test.

$ chmod +x hook.py
$ echo '{"name":"value"}' | ./hook.py
{"name": "value"}
$ echo $?
0

Here the hook script was made executable, then sample JSON {"name":"value"} is provided as input. The script emits the JSON unmodified as output, and the exit code is zero. This script works.

Now we add logic to the script to make it do something.

Implementation

For the implementation, the script needs to look for bug numbers. Taskwarrior bug numbers can be represented with a regular expression like this:

\b(tw-\d+)\b

The script is now modified to import re, and perform the substitution on the description attribute. By comparing the original description to the modified description, the script knows when to provide feedback. Here is the updated script:

#!/usr/bin/env python

import sys
import re
import json

added_task = json.loads(sys.stdin.readline())
original = added_task['description']
added_task['description'] = re.sub(r'\b(tw-\d+)\b',
                                   r'https://github.com/GothenburgBitFactory/taskwarrior/issues/\1',
                                   original,
                                   flags=re.IGNORECASE)
print(json.dumps(added_task))

if original != added_task['description']:
    print 'Link added'

sys.exit(0)

Testing the script again with better input yields this:

$ echo '{"description":"foo tw-179 bar"}' | ./hook.py
{"description": "foo https://github.com/GothenburgBitFactory/taskwarrior/issues/179 bar"}
Link added
$
$ echo $?
0

The script has correctly identified the bug number, and replaced it with the correct URL. The feedback message indicates this. We are ready to install this hook script and test it using Taskwarrior.

Install and Enable

To install the script, copy it to the ~/.task/hooks directory, creating that directory if necessary, and make sure the script is executable. It must also be associated with an event, which is done by naming it on-add*.

$ mkdir -p ~/.task/hooks
$ cp hook.py ~/.task/hooks/on-add-bug-link.py
$ chmod +x ~/.task/hooks/on-add-bug-link.py

There is a configuration setting that enables/disables hooks and you'll need to make sure hooks are enabled, although this is the default value:

$ task _get rc.hooks
on

Now run the diagnostics command, which will summarize the hooks it finds:

$ task diag
...
Hooks
    Scripts: Enabled
             <user>/.task/hooks/on-add-bug-link.py (executable)
...

We see that the hook script is found by Taskwarrior. Now let's see it in action, and note that the -- terminator is being used so that tw-179 is not perceived as a mathematical expression:

$ task add -- Contains no bug number
Created task 181.
$ task add -- Fix tw-179
Created task 182.
Link added
$
$ task _get 182.description
Fix https://github.com/GothenburgBitFactory/taskwarrior/issues/179

It works, but we have done minimal testing here. If you write a hook script with any non-trivial capabilities, your testing should be much more thorough. This is only an example.

Debugging

Taskwarrior has a hook debug configuration setting, which will show you how Taskwarrior processes the hook input and output, what happened, and how long it took. Here a similar task is added with debug information requested. The output is edited to show just the relevant hook information.

$ task rc.debug.hooks=2 add -- Fix tw-98765
...
Found hook script <user>/.task/hooks/on-add-bug-link.py
...
Hook: Calling <user>/.task/hooks/on-add-bug-link.py
Hook: input
  {"description":"Fix tw-98765","entry":"20150301T154518Z","modified":"20150301T154518Z","status":"pending","uuid":"daa3ff05-f716-482e-bc35-3e1601e50778"}
Timer Hooks::execute (<user>/.task/hooks/on-add-bug-link.py) 0.031061 sec
Hook: output
  {"status": "pending", "entry": "20150301T154518Z", "uuid": "daa3ff05-f716-482e-bc35-3e1601e50778", "description": "Fix https://github.com/GothenburgBitFactory/taskwarrior/issues/98765", "modified": "20150301T154518Z"}
  Link added
Hook: Completed with status 0
...
Perf task 2.4.2 f0cc015 20150301T154759Z init:3388 load:2001 gc:0 filter:0 commit:230 sort:0 render:0 hooks:33565 total:39184

Created task 183.
Configuration override rc.debug.hooks:2
Link added

The output shows that the hook script was found and run, the input and output is show, along with timing information, feedback and the status.

In this case the hook script ran in 31ms, which is certainly fast enough to not cause the user to wonder what is happening. In this example all hook processing was completed in 33ms.

Distribute

With your hook script complete, will you be sharing your script? It's optional of course, but if you do, consider a license and copyright, establish a web presence so it can be found and downloaded, perhaps put contact info in the script so you can be told of problems, then tell people about it.

You can tell us about your hook script, because we'd like to list it on the Tools page, along with many others.