git-remote-tasks

Git remote helper · Python, standard library only · MIT

Your task tracker, as a git remote.

git fetch jira-work pulls every issue in as a file under tasks/. Edit one in your editor, commit, git push — and the issue changes on Jira. Vikunja, Microsoft To Do and Notion the same way. Every sync is a commit, so diff, log, blame, bisect and tags work on tasks.

4trackers: Jira, Vikunja, Microsoft To Do, Notion
1Python file, no dependencies, four names by symlink
2file formats: YAML or Org-mode, chosen at init
A task tracker as a git remote: fetch imports tasks as files, push exports edits as API calls

A git remote helper is any executable named git-remote-<scheme> on the PATH: git spawns it for a URL with that scheme and speaks a line protocol over stdin and stdout. This one translates between each tracker's REST API and git's object model — nothing else in your workflow changes.

Every sync is a commit

refs/remotes/<remote>/main is a real linear history based on the previous tip. git diff main jira/main shows what changed upstream; git log -- tasks/ is the audit trail; git bisect walks every sync.

YAML or Org-mode

One format per repository, chosen at init. YAML diffs one property per line; Org gives Emacs and neovim agenda, DEADLINE: lines and [#B] priorities. Both round-trip through one schema.

Incremental

A full snapshot on the first fetch, a changed-since delta afterwards, with tombstones for deletions. The since token is recorded only once the tip has advanced, so an interrupted import never loses data.

Honest push

One upsert per edited file, one delete per removed file. A cross-source id is refused rather than duplicated; ok or error per ref, and git push's exit code reflects every failure.

Credentials in .git/config

Tokens live under [tasks-remote "<name>"], never in the URL. check <remote> validates the required keys without touching the network and redacts the secret-like ones.

Nothing to install but itself

Python 3.10+ and git 2.20+. install --bin-dir drops the file and the four symlinks on your PATH; msal is the one optional extra, for To Do's device-code login.

How it works

Two directions of git's remote-helper protocol, one file answering both.

  1. git fetch: capabilities, list, import

    The helper decides between a full snapshot — deleteall and a rebuilt tasks/ tree — and a delta: M for changed files, D for tombstones.

  2. A fast-import stream

    Written to stdout, based on the previous remote tip with from <sha>. Git builds the commit; the new since token is persisted only when the tip has advanced.

  3. git push: a fast-export stream

    Git writes the commits to push. Each M under tasks/ is deserialised and upserted on the tracker; each D is deleted; anything outside tasks/ is ignored.

  4. ok or error, per ref

    Every failure is reported by name and the exit code is non-zero if anything failed; nothing is retried behind your back.

Quick start

Python 3.10+ and git 2.20+. Credentials go into the repository's .git/config with git config; the URL only carries the scheme.

Install and initialise

git clone https://github.com/mmdemirbas/git-remote-tasks.git ~/src/git-remote-tasks
python ~/src/git-remote-tasks/git_remote_tasks.py install --bin-dir ~/.local/bin

tasks-init --format yaml ~/work/tasks && cd ~/work/tasks
git remote add vikunja vikunja://localhost:3456
git config tasks-remote.vikunja.scheme    vikunja
git config tasks-remote.vikunja.baseUrl   http://localhost:3456
git config tasks-remote.vikunja.apiToken  $(pass show vikunja/api)

Schemes: jira://company.atlassian.net, vikunja://host, mstodo://consumers, notion://db-<id>. The host part is informational.

Fetch, edit, push

git fetch vikunja
git merge vikunja/main --allow-unrelated-histories   # first time only

$EDITOR tasks/vikunja-42.yaml
git add tasks/ && git commit -m "tasks: raise priority on 42"
git push vikunja main                                # updates task 42

python git_remote_tasks.py check vikunja validates the configuration without a network call.

The git workflow

Ordinary git commands, and what each means for tasks.

Git commandEffect
git fetch <remote>Pulls the remote task snapshot into <remote>/main
git diff main <remote>/mainWhat changed upstream since the last sync
git merge <remote>/mainMaterialises remote tasks into the working tree
git log -- tasks/The audit trail of every sync and manual edit
git push <remote> mainUpserts edited tasks; deletes removed files
git tag release/2026-W38A snapshot of task state at a moment in time

What a task looks like

tasks/<source>-<native-id>.yaml — or .org — one file per task, every field on its own line.

# tasks/jira-PROJ-123.yaml
id: jira-PROJ-123
source: jira
title: Rotate the signing key before the audit
description: |
  The current key expires on the 14th; the audit starts on the 7th.
status: in_progress
priority: high
created_date: "2026-09-02T09:14:00+00:00"
due_date: "2026-10-01"
updated_date: "2026-09-18T16:40:12+00:00"
tags:
  - security
  - q4
category:
  id: PROJ
  name: Platform
  type: project
url: https://company.atlassian.net/browse/PROJ-123

Configuration

Everything under [tasks] and [tasks-remote "<name>"] in the repository's .git/config. The keys that matter most; the full reference is in the README.

KeySchemeMeaning
tasks.formatyaml or org, set once at init
schemealljira, vikunja, mstodo or notion
baseUrl, apiToken, emailjira, vikunjaService URL and credentials; email for Atlassian
projectKey / projectIdjira / vikunjaRequired to create new issues or tasks on push
jqljiraOverride the default filter
tenantId, clientId, defaultListIdmstodoMSAL tenant and app; the list for create and delete
databaseId, tokennotionTarget database and integration token
syncOverlapSecondsjira, vikunja, notionSubtracted from the persisted since so events that landed mid-fetch are not missed; default 5