# Zeit: a command line time tracker built around natural arguments

> Zeit is a Go time tracking tool for the terminal that records blocks of time against projects and tasks, and its distinguishing idea is a natural argument syntax, so a start command reads like a sentence and the same structure works as a filter. It ships as a single static binary for most Unix systems.

**mrusme/zeit** — Zeit, erfassen. A command line tool for tracking time. (https://tty.fail/mrus/zeit)

- Repository: https://github.com/mrusme/zeit
- Website: https://zeit.observer
- Stars: 593 · Forks: 30
- Language: Go
- License: NOASSERTION
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/mrusme-zeit

## A time tracker for people who live in a terminal

Zeit is described as a command line tool for tracking time spent on tasks and projects, and the name is German for time, with erfassen meaning to record. The download link in the project documentation covers macOS, Windows, Linux, DragonFly BSD, FreeBSD, NetBSD, OpenBSD, illumos, Solaris and Plan 9, and the homepage is https://zeit.observer.

That platform list says more about the design than the description does. This is a single static binary, built with CGO disabled according to the Makefile, distributed for every Unix variant worth supporting. There is no server to run and no account to create.

The primary language is Go, the module path is the punycode form of the project domain, and the dependencies in go.mod tell you what kind of program it is. cobra and viper-era flag handling provide the command structure, lipgloss v2 and the charmbracelet packages handle the terminal rendering, go-isatty detects whether output is going to a terminal, adrg/xdg locates the configuration and data directories per platform, and markusmobius/go-dateparser is what makes expressions like four hours ago work.

The storage layer is dgraph-io/badger v4, an embedded key-value store written in Go. That single choice explains a lot about the tool's character: the data lives on your disk, in a directory the xdg library picks, and there is no network component in the design at all.

## Natural arguments, and why they beat flags here

The most interesting decision in the project is the command line syntax, and the documentation argues for it explicitly.

Zeit v1 supports command line flags, but its primary focus is natural command line arguments. The example given is a start command that reads like a sentence.

```sh
zeit start block \
  with note "Research: Coca-Cola Colombian death squads" \
  on personal/knowledge \
  4 hours ago \
  ended 10 minutes ago
```

That tracks a new block of time with a note, on the personal project and the knowledge task, starting four hours ago and ending ten minutes ago. The documentation notes that using a more natural approach to command line arguments significantly enhances a user's understanding of the command.

The same operation is also expressible with flags, and the documentation is clear that both exist.

```sh
zeit start \
  --note "Research: Coca-Cola Colombian death squads" \
  --project "personal" \
  --task "knowledge" \
  --start "4 hours ago" \
  --end "10 minutes ago"
```

The reason both forms exist is spelled out too. If you use Zeit daily you may find the natural arguments interface more intuitive than working with flags, but if you are building a tool that interacts with zeit to inject or extract data, you will likely prefer the more programmatically robust flags.

That is the right way to build a tool with two audiences, and it is a good deal more work than picking one syntax.

## The same grammar does double duty as a filter

The argument structure is kept almost identical across commands, and that is what lets it serve as a filter rather than just a way to write a command.

```sh
zeit blocks \
  on personal/knowledge \
  from last week \
  until two hours ago
```

This lists all tracked time blocks for the personal project and the knowledge task, from last week at this time until two hours ago today. And the same structure works with the flags on the blocks command.

```sh
zeit blocks \
  --project "personal" \
  --task "knowledge" \
  --start "last week" \
  --end "two hours ago"
```

The design claim in the documentation is that the need for a detailed explanation is minimal, because the command's purpose is easily understood just by looking at it. That is the real payoff of the natural syntax, and it is more than a cosmetic difference in how the command looks. A tool you can read without a manual is a tool you will actually keep using.

The time expressions themselves are handled by the date parser library, which is why last week and two hours ago work as arguments to a filter without any date format specification.

## Projects, tasks, blocks and the simplified ID

The data model is documented as a set of key entities, and it is small enough to hold in your head.

The config entity is a fixed-key entity holding user configuration. A project is a top-level project. A task is a task underneath a project or another task, so tasks nest. A block is a tracked time period with a start and an end date and time that references a project or task by SID, which stands for simplified ID. An activeblock is a fixed-key entity containing the current, actively running block.

The SID is the interesting part. It is a simplified ID that matches a specific regex and is auto-generated, for example during import from a project's or task's display name, by removing whitespaces and other special characters. So a project named Knowledge Management becomes something like knowledgemanagement, and that string is what appears in the natural command syntax as personal/knowledge.

The other property worth knowing is that the project or task does not have to pre-exist. It can be created on the fly when you start tracking a new block, and configured afterwards using the zeit project and zeit task commands. For a tracker that is used casually, that removes a setup step that would otherwise sit between you and recording the time.

The nesting of tasks under other tasks, and the fixed-key config and activeblock entities, are the pieces that make the data model more than a flat list of durations.

## The v0 to v1 migration breaks your database

There is a hard incompatibility between the current release and the 0.x line, and the documentation is emphatic about it.

The latest Zeit version is incompatible with v0 releases. If you are upgrading from v0 to v1 or later, you must first export your existing v0 database.

```sh
zeit export --format zeit > ~/zeit_export.json
```

Only then do you upgrade to the latest release and import the exported database.

```sh
zeit import -f v0 ~/zeit_export.json
```

The order matters, and the reason is obvious once you look at it: the import command has to run on the new binary, and the old binary has to still be around to produce the export. The export format is named zeit, and the import takes a -f v0 flag to say which format the file is in.

This is the kind of thing that is easy to get wrong, since the failure mode of doing it in the wrong order is an unreadable database rather than an error message. If you have a v0 database, do the export first.

## Shell completions, a dmenu picker and the waybar module

Three integration points are documented, and they are what make the tool feel native rather than bolted on.

Auto-completion is generated for the shell of your choice and can be loaded into the current session directly.

```sh
source <(zeit completion $(basename "$SHELL"))
```

The supported shells are bash, zsh and fish, with PowerShell handled separately. For persistent loading, the completion scripts are written into the standard completion directories, one command per shell.

```sh
zeit completion bash > ~/.local/share/bash-completion/completions/zeit
```

```sh
zeit completion zsh > ~/.local/share/zsh/completions/zeit
```

The second integration is the dmenu compatible project and task selector, and it is the most useful of the three for anyone who wants to start tracking without typing a project name. It requires jq to be installed and a dmenu-style launcher, such as rofi or bemenu, to be set as DMENU_PROGRAM.

```sh
zeit projects -f json | jq -r '.[] | .sid as $parent_sid | .tasks? // [] | .[] | "\($parent_sid)/\(.sid)"' | $DMENU_PROGRAM
```

That pipes the project and task tree as JSON through jq into a fuzzy picker, and the selection becomes the argument you would otherwise type.

The third is a Waybar module, zeit-waybar-dmenu.sh, listed in the integrations table as a custom Waybar module for Zeit.

There is also a development switch worth knowing. Setting ZEIT_DATABASE to an empty string activates a non-persisting in-memory database, and the global --debug flag enables additional command line output. Both are aimed at development and testing rather than daily use.

## Where the local single-user model stops

Zeit is a local tool and the documentation never pretends otherwise, so the boundaries are worth stating before you commit.

The first is that there is no sync. The data lives in a Badger database in a local directory, the location determined by the xdg library, and the documentation describes export and import as a manual operation for a version migration. If you track time on a laptop and a desktop, you have two databases. The import format exists, so moving data is possible, but it is a file operation, not a synchronisation.

The second is that it is single-user. There are no accounts, no teams, no permissions and no shared reports. The features list is about blocks, timeouts, cron schedules and worker processes, none of which need a concept of a person.

The third is the dependency set. The good news is that it is all in-process and local, and the go.mod shows no network client library, no database server driver and no cloud SDK. The bad news is that the changelog and the releases, v1.0.1 in October 2025, v1.0.2 in July 2026 and v1.1.0 in August 2026, show a young project, and the last push was on 2026-09-09, so the data format and the CLI surface may still move.

The fourth is the licence, and it is unusual. The repository licence field shows a custom licence that GitHub cannot classify, and the badge in the project documentation points to a SEGV LICENSE at version 1.1 rather than an OSI identifier. That is worth reading before you use it commercially, because a non-standard licence can carry terms that a permissive one does not, and it is not something to assume.

## Conclusion

Adopt Zeit if you track time from the terminal and want a tracker whose commands read like plain sentences, with projects, tasks and blocks stored locally in a single binary. Do not adopt it if you need multi-device sync, a team dashboard or an invoicing workflow, because the documentation describes a local single-user tool. Before you upgrade from v0, export your database with zeit export --format zeit and import it with zeit import -f v0, since the latest version is incompatible with the 0.x releases.

## FAQ

### How do I build and install Zeit?

The project is built with make, which reads the version from git describe --tags or falls back to dev when no tag is reachable, and you can set it explicitly by prefixing the command with VERSION=x.y.z. Prebuilt binaries are published for macOS, Windows, Linux and a range of BSD and Unix variants.

### What is the difference between the natural arguments and the flags in Zeit?

Both express the same operation, for example starting a block with a note, project, task, start and end. The natural form reads like a sentence, while the flags are described as more programmatically robust and are the better choice if you are building a tool that interacts with zeit.

### How do I migrate my Zeit v0 database to the current version?

The current version is incompatible with v0 releases, so export first with zeit export --format zeit > ~/zeit_export.json using the old version, then upgrade and run zeit import -f v0 ~/zeit_export.json. Doing it in the other order risks an unreadable database.

### Where does Zeit store its data?

In an embedded Badger key-value store, with the location determined by the platform data directory convention through the xdg library. For development purposes, setting ZEIT_DATABASE to an empty string activates a non-persisting in-memory database instead.

### How do I select a project quickly in Zeit?

There is a dmenu compatible selector that pipes the project and task tree as JSON through jq into a dmenu-style launcher set as DMENU_PROGRAM. The resulting selection is the project and task argument you would otherwise type by hand.

## Sources

- [Issues](https://github.com/mrusme/zeit/issues)
- [mrusme/zeit on GitHub](https://github.com/mrusme/zeit)
- [Project website](https://zeit.observer)
- [README](https://github.com/mrusme/zeit/blob/master/README.md)
- [Releases](https://github.com/mrusme/zeit/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/mrusme-zeit
