No description
  • Shell 59.6%
  • Nix 40.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-21 04:19:01 +00:00
.claude-plugin plugin 2026-09-28 11:47:46 +02:00
.forgejo/workflows ci: keep upstream version and hashes in sources.json 2026-10-12 16:48:08 +02:00
plugin update 2026-10-12 16:22:19 +02:00
.gitignore Package zeit as a standalone flake 2026-09-01 20:12:50 -04:00
alejandra.toml Package zeit as a standalone flake 2026-09-01 20:12:50 -04:00
claude-plugin.nix plugin 2026-09-28 11:47:46 +02:00
flake.lock chore(flake): update flake.lock 2026-10-21 04:19:01 +00:00
flake.nix ci: keep upstream version and hashes in sources.json 2026-10-12 16:48:08 +02:00
hm-module.nix plugin 2026-09-28 11:47:46 +02:00
package.nix ci: keep upstream version and hashes in sources.json 2026-10-12 16:48:08 +02:00
README.md feat(zeit): 1.0.2 -> 1.1.0 2026-10-13 04:19:04 +00:00
sources.json feat(zeit): 1.0.2 -> 1.1.0 2026-10-13 04:19:04 +00:00
update.sh ci: keep upstream version and hashes in sources.json 2026-10-12 16:48:08 +02:00
zeit-wrapper.sh Package zeit as a standalone flake 2026-09-01 20:12:50 -04:00

zeit 1.1.0

A Nix flake packaging zeit, a command line tool for tracking time spent on tasks and projects, plus a Claude Code plugin that records every Claude Code turn in it. Upstream zeit lives at https://tty.fail/mrus/zeit.

On top of a plain buildGoModule it adds two things:

  • Shell completions for bash, zsh and fish, generated at build time.
  • zeit-wrapper.sh, which creates the badger database on first use. Every read-only zeit command opens the database read-only and badger refuses to open one that does not exist, so on a fresh machine every read fails with "no manifest found" until some write command has run. The wrapper imports an empty JSON document once, which brings the database into existence and stores nothing. The script itself carries the longer explanation.

zeit is under the SEGV License, which restricts fields of use and so is unfree by nixpkgs' reckoning. This flake sets allowUnfree for its own outputs; consumers using overlays.default build against their own nixpkgs and have to allow it there.

Usage

{
	inputs.zeit.url = "git+https://git.cumsek.com/flakes/zeit";
}

Then either take the package directly:

environment.systemPackages = [inputs.zeit.packages.${pkgs.system}.default];

or add the overlay and use pkgs.zeit:

nixpkgs.overlays = [inputs.zeit.overlays.default];

Home Manager

homeModules.default installs zeit and, optionally, the Claude Code plugin:

{inputs, ...}: {
	imports = [inputs.zeit.homeModules.default];

	programs.zeit = {
		enable = true;
		claudeCode.enable = true; # needs programs.claude-code.enable
	};
}

claudeCode.enable registers the plugin through programs.claude-code.plugins and adds the Bash(zeit-claude start:*) permission rule so Claude can label turns without asking each time (claudeCode.allowLabelling = false leaves the rule out). claudeCode.defaultTask sets the task that unlabelled turns go under.

Claude Code plugin

The plugin in plugin/ records each Claude Code turn as one zeit block, from the moment a prompt is submitted until Claude stops generating:

sequenceDiagram
	participant U as User
	participant H as Hooks
	participant C as Claude
	participant Z as zeit
	U->>H: submit prompt (UserPromptSubmit)
	H->>H: remember start time, project from git root
	H->>C: additionalContext: label this turn
	C->>Z: zeit-claude start -t debugging -n "..."
	Note over Z: block opened, backdated to the prompt
	C->>H: finish generating (Stop)
	H->>Z: zeit end

zeit cannot rename a block's project or task after it is created, so the hook does not open a placeholder block. Claude opens the real one, backdated so the time before its first tool call is still counted.

  • Project defaults to the git repository's directory name (or the working directory's if there is no repository), lowercased. Claude can pass -p to pick another.
  • Task and note come from Claude. The prompt hook lists the task names that already exist so Claude reuses them.
  • Unlabelled turns, such as a quick answer with no tool calls, are logged after they finish under chat (or $ZEIT_CLAUDE_DEFAULT_TASK), with the first line of the prompt as the note. They are skipped if another block is running, because zeit would cut that block short to make room for a backdated one.
  • Interrupted turns don't trigger the Stop hook, so the next prompt, or the end of the session, closes their block.
  • Concurrent sessions keep separate state under $XDG_STATE_HOME/zeit-claude/, and each session only closes the block it opened.

A labelled turn takes over the timer: a block you started by hand before the prompt is closed at the prompt, and nothing is left running when the turn ends.

Installing without Home Manager

This repository is also a Claude Code marketplace. The plugin runs zeit, jq and git from PATH, so those need to be installed:

/plugin marketplace add https://git.cumsek.com/flakes/zeit.git
/plugin install zeit@zeit

To load it for a single session, point --plugin-dir at a checkout or at the Nix package, which brings its own zeit and jq:

claude --plugin-dir ./plugin
claude --plugin-dir "$(nix build --no-link --print-out-paths git+https://git.cumsek.com/flakes/zeit#claude-plugin)"

Plugins cannot grant permissions. To stop Claude asking before each label, add this to ~/.claude/settings.json:

{"permissions": {"allow": ["Bash(zeit-claude start:*)"]}}

Updating

.forgejo/workflows/update.yml runs daily at 04:17 UTC (and on demand from the Actions tab): it updates flake.lock, points sources.json at the newest upstream tag with update.sh (rewriting the version in this title to match), and pushes to main only if nix flake check passes and zeit version reports the new version. The lock and the zeit bump land as separate commits.

By hand, the same thing is:

nix flake update
nix shell --inputs-from . nixpkgs#git nixpkgs#curl nixpkgs#jq -c ./update.sh
sed -i "1s/.*/# zeit $(nix eval --raw .#zeit.version)/" README.md
nix flake check -L

sources.json holds version, hash, vendorHash, and the commit and date that make would bake into runtime.{Version,Commit,Date} via -ldflags -X. Those two are recorded rather than read from git so the build stays reproducible. The version is taken from the git tags, not the Gitea releases feed, because upstream does not cut a release for every tag. The hashes are the ones nix reports when update.sh builds zeit.src and zeit.goModules with a fake hash, so they always match the fetcher.

Checks

nix flake check -L

Builds zeit and the plugin, plays Claude Code turns through the packaged plugin against a throwaway database (plugin/tests/turns.sh), runs alejandra, deadnix, statix and shellcheck, and fails if this README's title does not match the packaged version. .forgejo/workflows/check.yml runs the same command and nothing else.

Licence

The packaging and the plugin are trivial; zeit itself is under the SEGV License 1.1, as recorded in meta.license.