This is Profpatsch's personal development repository containing experimental projects and utilities.

NEVER use delegation to subagents unless I explicitely ask for it.

When the user tells you to use INSTRUCTION, you must ALWAYS run the instructions between :

<GIT_COMMIT> Read the file ./.claude/CLAUDE-git EVEN IF YOU READ IT BEFORE, and create a git commit </GIT_COMMIT>

<USE_RELATIVE_TMP> If you need to write temporary files, put them into ./tmp from where you are currently working. Don’t use /tmp </USE_RELATIVE_TMP>

Many projects here document themselves in a manpage next to the source, not in a README: mailweb.1, source-forge.1, timetrack.1, buildGo.7 and a dozen others. They are the authoritative description of what the thing does and, more importantly, why — including the constraints and caveats that are not visible from the code.

Before changing a project, read its manpage if it has one, even if you have read the source:

ls users/Profpatsch/<project>/*.[1-9]          # is there one?
man ./users/Profpatsch/<project>/<project>.1   # read it

Reading only the code means rediscovering by experiment what a paragraph would have told you, and re-introducing bugs the caveats section already warns about.

When you change behaviour the manpage describes, update it in the same change. A manpage that documents a flag that no longer exists is worse than none. Check your work with mandoc -T lint <page> (available via nix-shell -p mandoc).

The following projects are active projects I’m working on:

./users/Profpatsch/git-blimey :: A git-blame TUI written in go ./users/Profpatsch/timetrack :: A freelancing timetracker for multiple clients

Two rules govern the package tree (see ./README.md for the long version):

  1. <dir>/default.nix — this directory is a package, a function of { depot, pkgs, lib, ... }.
  2. <dir>/packages.nix — this directory is a namespace; it names its contents explicitly, one import per entry.

A directory that no packages.nix names is not in the tree at all. There is no filesystem traversal and nothing is injected implicitly: to find where depot.users.Profpatsch.foo comes from, follow the imports from ./default.nix.

Adding a package = write <name>/default.nix + one line in the parent packages.nix. Forgetting the second step is why a new package "does not exist".

module.nix is a NixOS module (imported by path from machines/), never a package. Adding a package to flake.nix is a separate, deliberate step; that list is explicit on purpose (see the comment there).

For projects that have .service files (systemd user services):

# 1. List packages installed in the user profile
nix profile list

# 1.2.a Upgrade package if installed
nix profile upgrade <package-name>

# 1.2.a Install from flake if not installed
nix profile install .#<package-name>

# 2 only if service does not exist yet, else 3
# 2.1 Copy the service file to systemd user directory
cp ~/kot/Profpatsch/users/Profpatsch/<service>/<service>.service ~/.config/systemd/user/

# 2.2 Reload systemd user daemon
systemctl --user daemon-reload

# 2.3 Enable and start the service
systemctl --user enable <service>.service
systemctl --user start <service>.service

# 3 Restart service if it was already running
systemctl --user restart <service>.service

Never launch a server, daemon or other long-running process with &, nohup, disown or similar. Always install it and drive it through its systemd user service, as above.

If a service must run with different flags for a test, edit the unit, restart it, and put it back afterwards — do not spawn a second copy alongside it. If you absolutely must have a second copy (e.g. to compare old versions), you may create a temporary second service (but remember to clean it again).

Note also that pkill -f <pattern> matches the shell running the command itself and will kill it mid-command, which looks like "pkill never works". Select the process explicitly instead:

ps -eo pid,args | grep '[m]ailweb --imap-host'   # bracket avoids self-match

NEVER use cd commands - Always use paths relative to the project in commands instead:

# BAD:
cd /home/philip/depot/users/Profpatsch && git add ./foo/bar

# GOOD:
git add users/Profpatsch/foo/bar

Git OperationsALWAYS read .claude/CLAUDE-git.md before any git commit If the user specifies "for ", only commit files related to the given project (in users/Profpatsch/<project>)

Deploying machines → Machine configs live in ./machines/profpatsch/ and are wired up as nixosConfigurations in flake.nix. Deploy straight from your local checkout (no push to any remote is required first):

# Deploy to any machine (runs nixos-rebuild switch on the remote)
deploy <machine>   # e.g. deploy legosi, deploy haku

# Or directly with nixos-rebuild from a local checkout:
sudo nixos-rebuild switch --flake ~/kot/Profpatsch#<machine>

The deploy binary (at users/Profpatsch/deploy/) builds the system closure locally, copies it to the target via nix copy, then activates it remotely.

NixOS machines: rolery (workstation), legosi, haku, leguin (servers)

Git remotes → The default push target is legosi-sources (ssh://source-forge@legosi:7001/var/lib/source-forge/Profpatsch.git; canon tracks it and remote.pushDefault is set to it), so a bare git push on canon pushes there and a post-receive hook re-ingests the tree into the source-forge source browser at sources.profpatsch.de. The website is a separate target and must be pushed by name (see below) — a bare git push does NOT publish the site.

source-forge can host any number of projects, each its own bare repo directly under /var/lib/source-forge/ on legosi (e.g. Profpatsch.git, netencode.git), published at sources.profpatsch.de/<project>/. A repo's post-receive hook is wired in generically (via core.hooksPath, not per-repo — see users/Profpatsch/source-forge/nixos-module.nix), but a project must still be declared before anything can be pushed to it — this is what decides, and locks in, which branch gets published:

ssh -p 7001 root@legosi 'sudo -u source-forge source-forge project add \
    --db /var/lib/source-forge/db/source-forge.db \
    --repos /var/lib/source-forge --project <name> --branch <branch>'

Pushing to an undeclared repo, or to a branch other than the one declared, is rejected (or simply ignored, respectively) — see source-forge.1 for the full project subcommand reference.

Deploying the website (profpatsch.de) → The website is NOT deployed via nixos-rebuild/deploy, and a bare git push (which goes to legosi-sources) does NOT publish it. It is served from a separate git remote on legosi: push the canon branch to the legosi-web remote by name (ssh://root@legosi:7001/var/www/web.git); a post-receive hook rebuilds and publishes the site:

git push legosi-web canon

The site sources live in users/Profpatsch/web/ (rendered HTML) and users/Profpatsch/blog/ (markdown source). Verify a change went live with the users/Profpatsch/link-check tool:

nix-build . -A users.Profpatsch.link-check
./result/bin/link-check https://profpatsch.de https://sfttime.softwaregardening.org

link-check is a recursive link checker: it crawls the live site, recurses into in-scope hosts, liveness-checks outbound links, and exits non-zero on dead links. NOTE: the site host is profpatsch.de (there is no www. record).

For rapid iteration with long-running REPL sessions (starting sessions, capturing large context, multi-line input, and the no-sleep anti-pattern), see .claude/CLAUDE-tmux.md.