Skip to main content
Yanked

This release has been yanked by its maintainers, and will be ignored by installers, except when explicitly specified.
Reason given by maintainers: pre-release, superseded

deckname-client

The workstation client for Deckname, a German DSGVO-compliant text anonymizer for Claude Code. It installs the Claude Code hook that replaces personal data in what a session reads with placeholders before it reaches the model, plus the commands that set a project up, sign the workstation in, check that protection works and start Claude Code.

The client talks to a Deckname server over HTTP only; detection runs on the server. Your organisation's Deckname administrator gives you the server's address. Python 3.11 or newer and Claude Code 2.1.121 or newer are required.

Installation

Install it as a tool, isolated from other Python packages, with one of:

pipx install deckname-client
uv tool install deckname-client

or into an existing environment:

pip install deckname-client

Without internet access, install the wheel file your administrator provides (it is attached to each release): pipx install deckname_client-<version>-py3-none-any.whl. Its four dependencies must then come from an internal package index or as wheel files too.

Check the installation:

deckname --version
deckname --help

Upgrade with pipx upgrade deckname-client, uv tool upgrade deckname-client or pip install --upgrade deckname-client.

First steps

In the project folder Claude Code should work in, run deckname with no arguments: it sets the project up (which files and tools are protected, which server is used), signs the workstation in, checks the protection and starts Claude Code. Step by step, the same is:

deckname setup      # declare what is protected, choose the server
deckname login      # sign this workstation in
deckname selftest   # show that this machine anonymizes
deckname launch     # start Claude Code behind the preflight

This page is the reference: one section per command, with the exit codes and stderr markers a rollout script can branch on. Every command's own --help carries its description.

What is installed

The console script is deckname; the server's is deckname-server. The two names are disjoint on purpose, so a machine with both installed shadows neither. A deckname-client compatibility command (pointed at the identical entry point as deckname) also stays installed, so a workstation set up under that older name keeps working until it re-applies its wiring — see "Upgrading a workstation set up under the old deckname-client command" below. On Windows, deckname-syncw runs the group sync service without a console window.

The client ships none of the detection stack (no Presidio, no spaCy model, no FastMCP). Its four runtime dependencies: pydantic-settings (the DECKNAME_HOOK_* settings model), questionary (the project-setup wizard's terminal UI, imported by that wizard alone), desktop-notifier (the group sync service's system notifications) and watchdog (the group sync service's folder watch) — the hook's per-call path stays stdlib + pydantic-settings.

The declaration file is deckname-protected-sources.toml (it was bpm-protected-sources.toml before). A workspace that has not renamed the file on disk is still read — with one deprecation warning naming the rename — so nothing loses coverage; if both files exist, the current name wins and a warning says so rather than choosing silently. Nothing in this package ever writes the retired name, and neither setup nor declare apply removes it: setup writing [paths] migrates the retired file's other sections onto the current name, so nothing it declared is lost, but it leaves the retired file itself in place — a workspace that had only the legacy file trips the "both exist" warning afterward instead of falling silent. declare apply never touches this file at all; it writes .claude/settings.json, not the declaration TOML (see "declare — generate the hook wiring instead of hand-editing it" below). Renaming or deleting the retired file by hand is the only way to silence the warning.

Configuration is read from DECKNAME_HOOK_* environment variables (plus the mirrored DECKNAME_MAX_INPUT_CHARS ceiling), or from a .env beside the workspace. The legacy BPM_* prefix is not read.

Claude Code 2.1.121 or newer — a hard prerequisite

This hook replaces a tool result by returning updatedToolOutput, and Claude Code honours that for built-in tools only from 2.1.121 onward. On an older host the hook is invoked, answers correctly, and the host discards the replacement: the declared file is read and its original content reaches the model. Nothing about that is loud — the hook does not error, so an older workstation looks configured and protects nothing.

deckname selftest checks this first, ahead of every other row, and reports RED below the floor and inconclusive when no claude can be found to ask. claude --version answers it directly; upgrade Claude Code if the number is lower.

The bare invocation — set up, verify, launch

Called with no arguments at all, deckname works on the current directory: it checks whether a deckname-protected-sources.toml and the matching hook wiring in .claude/settings.json are here, fills in what is missing with setup and declare apply, verifies the result, and then starts Claude Code behind the usual preflight. An already-configured directory writes nothing and goes straight to the preflight and the handover.

Upgrading a workstation set up under the old deckname-client command

The client's console script used to be deckname-client; a workstation set up under that name carries deckname-client hook --event ... inside .claude/settings.json. Today deckname is the client's primary console script, but deckname-client stays installed too, as a compatibility shim pointed at the identical entry point — so that retired command keeps resolving and the hook keeps covering content even before the wiring is regenerated.

deckname launch still refuses loudly rather than hand a session to claude while the wiring is stale — the shim is not something to depend on long-term, so it blocks, names the file, and points at the fix, instead of silently letting a workstation drift further from the current wiring. The one thing an operator must do by hand to remove the dependence on the shim:

deckname declare apply

That rewrites .claude/settings.json's hook commands to deckname hook and reports the rename on stderr, headed by [deckname:declare-migrated]. Nothing else needs a manual step: deckname-protected-sources.toml, the DECKNAME_HOOK_* environment and the .mcp.json registration are unaffected by this particular rename.

declare — generate the hook wiring instead of hand-editing it

deckname-protected-sources.toml in your workspace root declares what the hook protects. declare apply writes the hooks arrays and the DECKNAME_HOOK_* keys of .claude/settings.json from it; declare check reports drift and writes nothing. DECKNAME_HOOK_AUTH_TOKEN is never written — it stays in .env.

The token itself is the bearer token your Deckname server was configured with: put it in a .env beside the workspace as DECKNAME_HOOK_AUTH_TOKEN=<token>, and verify it with deckname selftest: the canary check makes the same authenticated POST /api/anonymize the hook makes, so a credential the server refuses surfaces there as a canary that could not be asked. setup prints this file as the place to read that, so it is the answer to "where does the value go".

deckname declare apply --workspace .
deckname declare check

declare check — exit codes and stderr markers (a stable contract)

These values are a published consumer contract: CI branches on the code and alert routing greps the marker, so they are stable once shipped.

Exit Meaning stderr marker Remedy
0 the generated region matches the declaration (silent) —
1 the generated region has drifted [deckname:declare-drift] re-run declare apply
3 deckname-protected-sources.toml does not parse [deckname:declare-source-malformed] fix the TOML by hand — declare apply cannot run until it parses
4 the deployment's published client policy cannot be applied [deckname:client-policy-malformed] fix the published policy on the server; nothing was written locally
5 a hook this client does not own claims a tool deckname's own hook claims [deckname:declare-hook-conflict] change that hook's matcher so it no longer selects those tools, or remove it; nothing was written locally

2 is left to argparse's usage error, so a mis-invoked command can never be mistaken for a malformed source. A workspace with no deckname-protected-sources.toml at all declares nothing and exits 0 silently. A successful declare apply that renamed a legacy shape says so with [deckname:declare-migrated], which heads no exit code of its own.

[deckname:declare-shell-uncovered] heads no exit code either, and reports a hole rather than a fault: the declaration protects files but leaves [bash] false, so whatever a shell command prints out of those files (cat, grep, type) reaches the model unredacted. Bash carries no path field — its tool_input is {command, …} and names no file — so it is covered at tool level or not at all, and a workspace that declares kuendigung.md while leaving the shell uncovered is one cat kuendigung.md away from delivering it in full. Leaving it uncovered is a legitimate choice; leaving it uncovered silently was not. Both declare check and declare apply report it, and neither changes its exit code because of it.

The rows above are held equal to the constants in deckname_client/declare.py (EXIT_CODE_MARKERS) by the test suite, in both directions.

The published client policy is fetched, not enforced

When the declaration names a [server] url (or DECKNAME_HOOK_SERVER_URL is configured), both declare apply and declare check fetch the deployment's published [mcp] / [web] floor from GET /api/client-policy and merge it into the workspace declaration before generating. This buys provability, not enforcement: the send/don't-send decision is client-side.

If that fetch fails, the run proceeds local-only with an empty policy — no cache, no hard failure, and its exit status decided by the local work alone — and one warning is written to stderr, headed by [deckname:client-policy-unavailable] and naming its cause (unreachable or auth-failed). Take it seriously: the declaration that run generates may be below the published floor, and beyond that one line nothing else will say so.

The hook re-reads the published policy on a short cache

The hook reads the same GET /api/client-policy on its own, on every mcp/web tool result, so a glob an administrator publishes takes effect on a workstation where nobody ran declare apply. Because the hook is spawned once per tool call, that answer is cached on disk for DECKNAME_HOOK_POLICY_CACHE_SECONDS (default 60s): a burst of tool calls costs one request, and a rule published now is in force within a minute.

The window is bounded in time and nothing else. An entry is used only while it is strictly younger than the lifetime, an entry dated in the future is discarded, and a failed fetch is never cached — so the cache can never be the reason a published rule is missed for longer than its lifetime. Set the variable to 0 to disable it and fetch on every call. The cached file lives beside the client's other per-user state (%LOCALAPPDATA%\deckname / $XDG_CACHE_HOME/deckname; bpm-anonymizer before that — a workstation carrying only the retired directory has it adopted automatically on the next hook run, so nothing is lost) and holds the two published glob lists and the review mode word only — no token, and no URL (the deployment it belongs to is recorded as a digest).

The git hook is a convenience; CI is the gate

scripts/git-hooks/pre-commit in the server repository runs declare check before a commit and passes its stderr through unchanged. It runs only for developers who install it themselves: the repository ships no installer and configures no core.hooksPath, so cloning does not wire it up. Install it by hand if you want it:

git config core.hooksPath scripts/git-hooks

The enforcing gate is the CI invocation of deckname declare check; the hook merely moves the same answer earlier. A missing client makes the hook exit 0 rather than block the commit.

declare scan — files that often hold secrets, offered as protected sources

deckname declare scan [--workspace ORDNER] [--yes] [--json] looks for the files an ecosystem conventionally keeps its credentials in and offers the ones the declaration does not protect yet as [paths] entries. It reads names only and prints paths only, never a file's content. Two files are opened without returning a byte of them: a config.json counts as Docker's only when it carries auths, and a *.csproj is read for its <UserSecretsId>.

Group (--json ecosystem) Files
Umgebungsdateien (env) .env, .env.* — not .env.example, .env.sample, .env.template
.NET (dotnet) appsettings.json, appsettings.*.json; the user secrets a project's <UserSecretsId> names (%APPDATA%\Microsoft\UserSecrets\<id>\secrets.json, ~/.microsoft/usersecrets/<id>/secrets.json elsewhere), declared by their absolute path
Spring (spring) application*.properties, application*.yml, application*.yaml
Azure Functions (azure_functions) local.settings.json
Schlüssel und Zertifikate (keys) *.pem, *.key, *.pfx, *.p12, *.jks, *.keystore, id_rsa*, id_dsa*, id_ecdsa*, id_ed25519* (not their .pub)
Paket-Registries (registries) .npmrc, .pypirc
Terraform (terraform) *.tfvars, *.tfvars.json, terraform.tfstate*
Zugangsdaten (credentials) .netrc, _netrc, .aws/credentials inside the project
Docker (docker) a config.json with auths
Kubernetes (kubernetes) kubeconfig, kubeconfig.*, *.kubeconfig, .kube/config

Which files are looked at. In a git checkout, git ls-files gives the tracked files, the untracked ones that are not ignored, and the ignored ones with every ignored folder collapsed into one entry: .env is usually ignored, and it is the file this scan exists for. An ignored folder is walked unless it is one of .git, node_modules, .venv, venv, dist, build, bin, obj, target. Outside git the same folders are skipped by a walk of at most 12 levels; either way the scan stops after 50,000 names (truncated in --json). A name carrying a glob character or a comma is never offered, since the declaration would read it as a pattern.

On a terminal the list is shown grouped and one question asks whether to add all of them („Als geschützte Quellen eintragen?“). --yes adds them without asking; without a terminal and without --yes the list is printed and nothing is written. --json prints one object (workspace, found, unprotected, added, truncated, exit) and never asks; it adds only with --yes. Adding goes through setup's own writer (union in, nothing removed) and then runs declare apply. Exit codes: 0 nothing found, the list shown, or everything added and wired; 1 nothing added — the question answered „Nein“ or cancelled, or the write failed ([deckname:declare-scan-error]); 3 the declaration does not parse ([deckname:declare-source-malformed]); after the files were added, declare apply's own codes.

setup — author the declaration on a workstation's first run

deckname setup [WORKSPACE] writes a workstation's deckname-protected-sources.toml before Claude Code is ever started, with no model in the loop. It scans the workspace for directory and file names only (never contents) and shows them as a tree: every folder that holds a file of a supported type, with how many and which types, and every supported file at the top level. Nothing is guessed or pre-ticked — the first question asks which of these to protect (and is skipped when there is nothing to show), and — through the same one writer declare reads back — setup writes what was chosen. Außerdem schützen? → Ordner oder Datei … takes a folder anywhere on the machine; when it holds a supported file, its own tree opens the same way, the whole folder first. The supported types live in one table, deckname_client.source_types.SOURCE_TYPES; adding a type (Excel, once the server parses it) is one row there, and both the tree and prepare pick it up. It is one row on the deckname console script (there is no deckname-setup binary), beside hook, declare, selftest and launch.

WORKSPACE defaults to the current working directory when omitted; pass a path to operate on a different one. Without --yes, setup opens the SAME guided, one-question-at-a-time conversation bare deckname opens on a fresh project — on the rail where the terminal can host it, as plain [J/n]-style lines where it cannot (Git Bash / mintty). With --yes (equivalently --non-interactive) it guesses nothing either: it keeps the sources the declaration already names (an MDM rollout ships the file), adds none, and never reads stdin.

Two more questions after the wiring (terminal only). When the project holds files that often carry secrets and the declaration does not protect them yet, setup lists them grouped by ecosystem and asks whether to add them — the same scan and writer as declare scan, asked only when something was found. Then, when this computer holds a deckname login for the project's server and its release has sync, setup lists your Akten and asks „Eine Akte auf diesem Computer synchronisieren?“: „Akte als Arbeitskopie synchronisieren“ (pick the Akte, then the same flow as deckname sync start "<Akte>"), „Ordner als Quelle synchronisieren“ (name the folder, then a new Akte named after it or an empty one you write in, then the same flow as deckname sync start --quelle <Ordner> [<Akte>]) or „Später“ (the default). Setup closes its own rail first and hands the answer to sync start, whose exit code stays its own; setup's code is unchanged. Without a login or sync, setup says the one line „Akten auf diesem Computer synchronisieren: deckname sync start.“ instead. --yes asks neither question: it prints that line, and, when the scan finds unprotected files, one line naming their count and deckname declare scan. Nothing is added on its behalf.

--server <url> writes the anonymizer's address into [server] url — the one thing setup authors outside [paths]. On a terminal the wizard asks that question itself, so --server is for the scripted path and is refused (nothing written) when it would open the wizard; pass it with --yes. The address is validated by the declaration reader before anything is scanned or written, so a value declare apply would later refuse never lands on disk.

Omitting the flag does not mean [server] is left unauthored. This paragraph said so until the refusal-surface work re-read the code, and it was wrong: every setup path writes [server] url. The value is --server's where one was given, else the address already declared here — preserved byte for byte, not rewritten — else the client's own shipped default http://127.0.0.1:8000. So a workspace setup has touched always names a server, which is why the run reports the two cases differently: "… eingetragen" when it wrote the key, "… bereits … eingetragen, unverändert übernommen" when it left an existing one alone.

And a successful setup applies the wiring itself. After writing the declaration it runs declare apply's own generator — the same function, never a second writer of the same file — so .claude/settings.json is in place when setup returns and one command reaches a launchable workspace. declare apply stays its own command for re-wiring: after a hand edit to the declaration, and as the remedy for exit 5 below, where the declaration is already correct and only the wiring step failed.

On Windows, the interactive path needs Windows Terminal or PowerShell. In Git Bash / mintty isatty() reports False, so this command cannot tell it has a terminal: it takes the scripted branch, the wizard is never opened, and the server question is never asked. Run python -c "import sys; print(sys.stdin.isatty())" to settle which shell you are in before reading a missing prompt as a defect. An interactive run whose stdin is not a terminal and which was not given --yes writes nothing and says so, under [deckname:setup-needs-a-tty] — the 1 row below.

deckname setup                    # interactive, over the current directory
deckname setup --yes ./workspace  # scripted (MDM / CI), a given workspace
deckname setup --yes --server https://anonymizer.example.de   # ... naming the service

setup — exit codes and stderr markers (a stable contract)

An MDM rollout scripts this command and branches on its status, so these values are a published consumer contract: stable once shipped. 3 reuses declare check's meaning — an unparseable deckname-protected-sources.toml — because one operator reads both.

Exit Meaning stderr marker Remedy
0 setup completed (a declaration was written, or a re-run confirmed it already correct) (the empty-scan note only) —
1 the run ended without writing (declined or aborted; an interactive run on a non-terminal stdin without --yes; a --server address the declaration reader refuses; or --server where the wizard would open) [deckname:setup-needs-a-tty] on the non-terminal case, [deckname:setup-server-unusable] on both --server cases — the refused address and the contradiction with the wizard, [deckname:setup-error] on a write that failed re-run, or pass --yes for an unattended run
3 an existing deckname-protected-sources.toml does not parse [deckname:setup-source-malformed] fix the TOML by hand; nothing was written
4 the workspace path is unusable (does not exist, or is a file) [deckname:setup-workspace-unusable] pass an existing workspace directory
5 the declaration was written and the wiring step that follows it refused (an unresolvable policy floor, or a .claude/settings.json the filesystem would not let the generator write) [deckname:setup-wiring-failed] fix the cause named above the marker, then run deckname declare apply — do not re-run setup, the declaration is already correct

Where a finding is said. On stderr — the stream the "is a person reading?" question is asked of. When stderr is a terminal each finding is ONE status line there, after the result block — ▲ 1 Dokument vom Server abgelehnt for a refusal (its whole sentence under -v; no pointer at deckname prepare -v, which would hand every document over again), a ▲ for a pattern that matched nothing, a ✖ for a server that could not be reached or a declaration that does not parse — and no marker is printed. The result block itself stays on stdout, so deckname prepare > bericht.txt at a terminal still shows every finding there. The marker is written when prepare's stderr is not a terminal (every CI and MDM run, and any caller that passes its own report sink), exactly as the table says; the exit code is the same either way.

2 is left to argparse's usage error, so a mis-invoked command can never be mistaken for a real outcome. A --yes run whose scan finds nothing writes an empty [paths] and still exits 0, saying so on stderr — a successful exit is not evidence the workspace is covered.

5 is the one code that does not mean "run setup again": the declaration is on disk and correct, and what is missing is the file declare apply writes. 1 means the opposite — the run wrote nothing — so a rollout script must branch on the two separately.

The rows above are held equal to the constants in deckname_client/setup_cli.py (SETUP_EXIT_CODE_MARKERS) by the test suite, in both directions, so this table cannot fall behind the contract again.

That equality is a membership check, and membership was not enough. It asks whether a marker is listed against a code, never whether the run that reaches that code actually prints it — and one refusal printed none at all: --server passed where the wizard would open exited 1 with no marker on stderr, so a rollout script greping for one read the run as an unexplained abort. The same module now also DRIVES each sub-case above through the real entry point and holds the pair it produces, and it refuses any published marker that no driven sub-case emits, which is the direction that would have caught it.

selftest — show that this machine anonymizes

deckname selftest runs the checks on this workstation and prints the result, with --json for an MDM or CI run that branches on it. It proves the local half only — it says nothing about a published policy; use declare check for that.

deckname selftest
deckname selftest --json

launch — preflight, then Claude Code

deckname launch runs the preflight, and either blocks on a red result (naming the check that failed) or hands over to claude with every argument forwarded unchanged. While the checks run, one line spins on a terminal (Schutz wird geprüft …); a green launch then prints one status line — ✔ Schutz aktiv 3 Orte · localhost:8765 · 11 Prüfungen bestanden: the declared places, the service's host and port, and the checks that passed — each warning as one ▲ line with a single Mehr dazu: deckname launch -v pointer (bare deckname points at deckname -v, its own long form), and Claude Code startet …. A status line with a warning under it leaves the count out (✔ Schutz aktiv 3 Orte · localhost:8765). Off a terminal there is no animation and no escape sequence, just those lines. A blocked launch prints four lines — ✖ Start abgebrochen: <was den Start verhindert hat>, Behebung: <der eine Schritt>, Geprüft: <was geprüft wurde> · Mehr dazu: deckname launch -v and, dim, Ohne diese Prüfung: claude direkt starten. (left out only when claude itself was not found) — then any warning in the same one-line form, and nothing else; it exits 2. With a warning after the block the pointer moves under the warning, said once. Strg+C while the checks run prints Abgebrochen. Nichts gespeichert. and exits 1 — the one line Strg+C prints at every question and wait in this client; after a first run's setup was saved it reads Abgebrochen. Die Einrichtung ist gespeichert. instead.

-v (or --verbose) right after launch is the launcher's, like --json: it prints the status line, every warning's full sentence, one blank line, the six-column log of the operations the preflight covered, and then the hand-over line — bare deckname -v does the same. On a blocked launch -v prints the long refusal instead of the three lines: the whole reason, the check's own detail and the way round it (running claude directly). The two flags form a leading run, each at most once and in any order; after any other argument, or after --, -v and --json are Claude Code's and are passed through unchanged (launch -p x -v asks Claude Code for its version, launch -- --json hands it --json). --json writes the log as one object per line instead of the grid, on every launch, with no spinner, status line or hand-over line.

deckname launch
deckname launch -v
deckname launch --json
deckname launch -- --resume

No line, in any form, carries a covered surface form or a full path — the status line included, which is computed from the checks' results and this workstation's own configured address, never from anything the self-test printed. claude can still be called directly at any time; the protection does not hang on this launcher.

prepare — hand declared documents to the async review queue, and say where they went

Asked about, by release stage. A deployment's release stage (DECKNAME_RELEASE) decides whether it accepts documents ahead of the read, through the feature prepare; V1 — stage 1 — does, and so does every later stage (stage 2 adds the group sync, stage 3 four-eyes review). prepare first asks the server (GET /api/me/config, its release object) once the workspace has something declared; on a stage without prepare it says „In dieser Version nicht verfügbar." and exits 2, and setup's and the first run's closing submit offer is not asked at all (and not even asked of the server when the workspace has nothing to offer). Only a readable answer is a stage: a server that cannot be reached, refuses the credential, fails (5xx), answers any other unexpected status, or sends something that is not a JSON object (a proxy's error page, an SSO login page) is reported as unreachable (5), never as "not available"; a readable JSON answer without a stage, or without prepare among its features (a server from before the feature existed), offers nothing.

deckname prepare reads the workspace's own deckname-protected-sources.toml, matches it against the files on disk through the same matcher the hook itself uses, and hands the matching documents to the deployment's already-live asynchronous upload queue for review (mode: async, destination: review) — one POST per document. It never anonymizes locally.

Name one or more paths to submit only those; a pattern such as docs/** is matched by the declaration's own glob engine, and a directory name means everything under it. With no path at all every declared document is submitted — except on a terminal, where one question is asked instead (Alle N / Einzeln auswählen … / Später) so the choice is made in front of you; Strg+C there prints Abgebrochen. Nichts gespeichert., submits nothing and exits 1. A pattern that matches no declared document submits nothing at all and exits 6: a partially-submitted run after a typo is how a person comes to believe four documents are queued when three are.

Unless --no-wait says otherwise, the run then follows its own jobs to a terminal state, prints one review address per parked document, and opens the review page once. That follow-through is the answer to a measured defect: a document queued by prepare used to park a review that nothing announced and nothing could have. On a terminal the wait is one spinning line; Strg+C there ends the wait and nothing else — the documents are handed over and their jobs keep running on the server — so the run says Abgebrochen. Die Dokumente sind übergeben. and exits 1.

What it prints is a short block on the rail: ✔ 12 übergeben · 3 mit unbekannter Endung übersprungen, then after the wait ✔ Zur Prüfung bereit with one link per review behind a │ (the browser tab the run opens is not announced: nothing can learn that it opened), and a refusal as ▲ 1 Dokument vom Server abgelehnt. The service's Ziel: address and the sentence about reaching it from elsewhere are -v's. Asked on a terminal, the question reads 12 deklarierte Dokumente zur Prüfung einreichen?.

What preparing buys, and the one condition on it. A document prepared and approved is the document the next real read serves: prepare sends the declaration digest this workstation has in force, the server files the approval under the same cache namespace the hook composes when Claude reads that file, and the edits you made while reviewing are what the read gets. The condition is that both halves match — the same workstation identity, and the same applied declaration. Edit deckname-protected-sources.toml without running declare apply, or prepare on one machine and read on another, and the read parks its own review again, which is the privacy rule working rather than a fault.

deckname prepare
deckname prepare CLAUDE.md
deckname prepare "docs/**" kunden
deckname prepare --no-wait
deckname prepare --workspace ./workspace

prepare — exit codes and stderr markers (a published contract)

These values are a published consumer contract, in the same style as declare check's table above: CI branches on the code and alert routing greps the marker, so they are stable once shipped. The bearer token this verb sends is the same DECKNAME_HOOK_AUTH_TOKEN that declare/selftest already read — see "declare — generate the hook wiring instead of hand-editing it" above for where that value goes.

Exit Meaning stderr marker Remedy
0 every matching document was handed over (or the workspace declares nothing at all) (silent) —
1 at least one document was refused by the server, after every other document was attempted — or Strg+C ended the run: at its question (nothing was submitted) or during its wait (the documents are handed over); a cancel writes no marker [deckname:prepare-refused] inspect the refused document, then re-run prepare; after a Strg+C, run prepare again when you want it
2 the deployment's release stage does not include prepare (DECKNAME_RELEASE; every shipped stage does, V1 included — a server from before the feature existed does not): „In dieser Version nicht verfügbar." — nothing was offered or submitted. argparse's usage error keeps the same number, without a marker [deckname:prepare-not-in-release] none on this workstation — the deployment's operator updates the server or raises the stage
3 deckname-protected-sources.toml does not parse — the identical reader and condition declare check reports [deckname:declare-source-malformed] fix the TOML by hand
5 the server could not be reached, or refused the credential — or answered the release question with an error, an unexpected status or an unreadable page [deckname:prepare-server-unavailable] check hook_server_url and DECKNAME_HOOK_AUTH_TOKEN, then re-run prepare
6 a named path or pattern matched no declared document; nothing was submitted [deckname:prepare-selection-unmatched] check the spelling against deckname-protected-sources.toml, or run prepare with no path
7 every document was accepted and at least one job produced no review (failed, cancelled, expired, or still running when the wait ended) [deckname:prepare-not-parked] look at the review page; a job still running needs no action, a failed one needs the document re-submitted

Where a finding is said. On stderr — the stream the "is a person reading?" question is asked of. When stderr is a terminal each finding is ONE status line there, after the result block — ▲ 1 Dokument vom Server abgelehnt for a refusal (its whole sentence under -v; no pointer at deckname prepare -v, which would hand every document over again), a ▲ for a pattern that matched nothing, a ✖ for a server that could not be reached or a declaration that does not parse — and no marker is printed. The result block itself stays on stdout, so deckname prepare > bericht.txt at a terminal still shows every finding there. The marker is written when prepare's stderr is not a terminal (every CI and MDM run, and any caller that passes its own report sink), exactly as the table says; the exit code is the same either way.

2 is also what a release stage without prepare answers: there the verb answers as the absent verb it is, and the marker is what tells that refusal from a mis-invoked command. 4 is deliberately skipped: it already means "the published client policy cannot be applied" in declare check's table above, and a number that means two things in this client is worse than a gap. A document refused by the server (1) and the server itself being unreachable or refusing the credential (5) are never conflated — the second is never counted as one more refused document. Nor are a refusal (1) and a job that did not park (7): the upload seam this verb consumes makes "a refusal is never a failed job" a rule of the contract, so a document the door took and then lost is never reported as a document the door turned away. 7 is unreachable under --no-wait, which never learns any job's outcome.

sync — a group's documents as a folder on this computer

One source, many working copies. Every group (German Akte) has at most one source: a Git repository, a OneDrive or SharePoint folder, or a folder on a computer. Every member may keep working copies of it on their computers, several per group and computer. A change in a working copy goes through the detection into the group and from there to the source and every other copy; a change at the source goes into the group and to every copy. Each folder holds the group's folder tree: every document at <Ordner>/<Name>, every folder of the group made there (an empty one too). A document with an original is its real file (Word, PDF, Markdown …), one without is its covered Markdown under a frontmatter naming the document and its fassung (deckname_doc, deckname_version). Set up entirely in the terminal, it asks only what is missing. deckname setup offers the same two ways at its end (see setup above):

deckname sync                                       # the status, or (nothing set up) your groups
deckname sync start "Musterprojekt Nordwind"         # a working copy, asks for the folder
deckname sync start "Musterprojekt Nordwind" "D:/Prüfung/Nordwind"  # a further copy, asks nothing
deckname sync start --quelle "D:/Projekte/Nordwind"  # this folder becomes a new group's source
deckname sync start --quelle "D:/Vertrieb" "Vertrieb Nord"  # the source of an empty group
deckname sync start --quelle "D:/Projekte/Nordwind Süd" --vorgang 7KQM  # started from the portal
deckname sync status [--devices] [-v]               # this computer; --devices: your others too
deckname sync pause [<folder|group>] / resume [<folder|group>]  # without one: every folder here
deckname sync resume "Musterprojekt Nordwind" --allow-deletions  # confirms held deletions
deckname sync stop "D:/Prüfung/Nordwind" [--yes] [--wipe]        # ends this one folder
deckname sync set --review-open notify_only         # immediately | after_2_min | notify_only

A working copy (start "<group>" [<folder>]) lies in the folder you name, which must be empty or new, by default in ~/deckname/<group> (C:\Users\<name>\deckname\<group> on Windows). start without a folder for a group that syncs here already says „Läuft bereits“; a further folder is a further copy (a device of its own on the server, its own lock). Two sync folders never lie inside one another. The output names the group, its source and the folders made („6 Ordner angelegt · wie in der Quelle“).

A source on this computer (start --quelle <folder> [<group>]) is a folder with files already. Without a group the client makes the group, named after the folder (the person becomes a member who writes); with a group it must be an empty group you write in (a group with documents gets its source from an admin in the portal). The folder is read with its subfolders („Ordner gelesen“ with the count per folder); Thumbs.db, Office's ~$… files and executables are left out („Übersprungen“). The client sends the list of files with their SHA-256 (the manifest): a file that is a document already is recorded, one where a document lies with other bytes is sent without a shared fassung (the replaced fassung's author gets an overlap, nothing wins silently), a new file goes up into the import's queue on the server („412 Dateien übertragen · Erkennen und Abdecken laufen auf dem Server“). No file of the folder is changed. A group with a source answers a second one with exit 6, the source's kind, place and owner, and the command for a working copy instead („Als Arbeitskopie: deckname sync start …“). The same folder keeps one folder id here, also after a stop, so the server knows it again.

Limits of a new source. Before it connects a folder as a source, the client asks the server how large one Akte's source may be (GET /api/sync/limits: files, bytes, the largest single file; the admin sets them under Einstellungen › Uploads & Anfragen). A file over the file limit is left out like a system file („werden übersprungen“). A folder with more files or bytes than allowed is refused with exit 7 and nothing is registered:

✖  Nicht gestartet: Die Quelle ist zu groß.
   64.200 Dateien und 26,1 GB. Erlaubt sind 50.000 Dateien und 20 GB je Akte.
   Bitte einen Unterordner wählen, zum Beispiel: deckname sync start --quelle "E:\Archiv\2026"

Large imports (up to about 50,000 files). The folder is read in one walk: each file is hashed as the walk reaches it, in chunks, so memory stays small however many and however large the files are, and a terminal counts along („Ordner wird gelesen … 12.400 Dateien · 3 GB“). The manifest goes in pages of 2,000 files (POST …/manifest?page=, last on the final page, each file with its size; „Dateien werden gemeldet … 8.000 von 30.000“); when the server lost the import between two pages (409 manifest_restart) the client starts again from page 1. The new files then go up four at a time („Dateien werden übertragen … 120 von 30.000“), each answered on its own as before (a refusal, a file too large, the deletion guard and the folder's lock are unchanged), and state.json is saved every 100 answers. Each upload of a source answers review without a review (queued): the file waits in the import's queue, and the client keeps it as sent until the group's listing names its document. The command ends once the files are sent; the import continues on the server:

│  Der Erstimport läuft auf dem Server im Hintergrund weiter. Fortschritt im Portal: Akte
│  „Nordwind“, Tab Synchronisierung.
│  Claude Code liest die Dokumente erst nach der Freigabe im Portal.

The server detects each file in the background. A document with findings waits for its Prüfperson in the Prüfplatz, one without findings for a release („Ohne Funde freigeben“); before that, nothing of it reaches Claude Code, search or the MCP tools.

Pause and cancel from the portal. The Akte's Prüfperson (or an admin) can pause the import („Anhalten“), resume it („Fortsetzen“) or cancel it („Abbrechen“) in the portal. While it is paused the server answers the source's manifest and uploads with 409 import_paused: the client stops sending, keeps what it holds, and tries again on its next pass; once the server takes the files again the rest goes up. A manifest cut off by a pause is kept beside state.json (names and digests, never a byte of a file) and sent again from page 1. After a cancel (409 import_cancelled) nothing more is sent until the person chooses „Erneut starten“ in the portal and runs deckname sync start --quelle "<Ordner>" again: the folder is read once more and every file goes up anew („Erstimport neu gestartet“, --json: "result": "restarted"). status shows it under the source („Erstimport Pausiert. …“ / „Abgebrochen. …“), --json as import_state (paused, cancelled or null).

Started from the portal (--vorgang <Code>). The portal's page „Quelle synchronisieren“, step „Ort“ for a folder on a Workstation, shows the command with a four-character code for that page (valid while the page is open, at most 30 Min.). The client reads the folder and reports it to the page („Ordner gelesen · 9 Ordner · 412 Dateien · 180 MB · 23 werden übersprungen“: the counts and up to 50 skipped files with their reason), then waits for the page („Weiter im Portal: Name der Akte festlegen.“). It waits on a server stream, not on a timer. When the page names the Akte („Akte anlegen und synchronisieren“, a new one or an empty one you write in), the client connects the folder as that Akte's source as above and ends with „Fertig. Fortschritt im Portal.“ When the page is closed first, the client says „Der Vorgang wurde im Portal beendet. Es wurde nichts geändert.“ and exits 1. An unknown, foreign or expired code is exit 6 („Der Vorgang … ist beendet oder abgelaufen.“). --vorgang needs --quelle and no group.

Folders both ways. A folder made, renamed, moved or removed in a sync folder is reported to the group (POST …/folders); a file moved or renamed there (the same bytes under a new name in one pass) is reported as a move (POST …/moves), so its fassungen and reviews stay with it; a folder renamed with its files is one rename. What the group changes comes back the same way: a new folder is made, a renamed one renamed with its files (a moved document is moved here, not fetched again), a removed one goes, what is left in it into the local trash.

The one-time move. A folder set up before the folder tree (<Ordner>/deckname/<group>/ with OneDrive/, Git/ and Lokal/ below it) moves on its first pass after the update: the folder registers again with a folder id, each document's file moves to where the group has it now, a file not sent yet moves up out of its source folder, and the old device is signed off. The same name twice with other bytes keeps both, the second named … (aus Git). status says so once („Ordner umgezogen“). The folder itself stays where it is.

Every subcommand takes --json: exactly one object on stdout, a failure included ({"error", "exit", …}), and no question is ever asked. Without a terminal no subcommand asks anything either; start then takes ~/deckname/<group>, and stop needs --yes. The home folder, not Documents: Windows' Known Folder Move puts Documents, Desktop and Pictures into OneDrive (macOS can put them into iCloud), which would carry the originals into a cloud in clear text. A folder you name inside OneDrive (also %OneDrive%, %OneDriveCommercial%, %OneDriveConsumer%), Dropbox, Google Drive or iCloud Drive is taken, with the warning „Ordner liegt in “ (--json: "cloud"). A folder set up before keeps its place. The server is --server, the workspace's own when run inside one, or the one this computer holds a deckname login for. start also takes a deckname://sync/<group_id> link (the web's „Im Client öffnen") in place of the group's name.

With read access every copy is read-only: a file changed or deleted on this computer is put back on the next pass, with a system notification „ · Änderung zurückgesetzt". A document deleted in the group is moved into <group folder>/.deckname-trash/ — the client carries no system-trash library, and a move inside the group's folder keeps the rule that the sync never writes anywhere else. A pass removes what lies there longer than 7 days.

With write access the files go back. A saved file is sent 5 seconds after its last change, only when it differs from the file the client wrote (PUT with the fassung it was written from). The server merges it with what came in meanwhile; without a new finding it is the next fassung at once and the merged file is written back. With a new finding it waits in your own Prüfplatz: the file stays as you saved it until the review is closed (approved: the new fassung is written; rejected: your change is discarded and the current fassung written). A new .md file becomes a new document (POST), a deleted file deletes the document from the group (DELETE, the group's trash keeps it 30 days). Offline the changes wait and go on the next contact. A review or an overlap shows a system notification („Risikoliste · neue Stellen", „Risikoliste · 1 Konflikt", action „Jetzt prüfen") and opens the Prüfplatz in the browser per deckname sync set --review-open: immediately, after_2_min (the default: once the file was not changed for 2 minutes) or notify_only; once per review or overlap, one page when both concern one document. An expired login pauses the sync („Synchronisierung angehalten", action „Anmelden"). Notifications use desktop-notifier; where the desktop offers none, nothing is shown and the sync goes on.

Protected from Claude Code. The folder holds the originals in clear text, so start (and deckname setup) writes the hook into your user-level Claude Code settings (<CLAUDE_CONFIG_DIR or ~/.claude>/settings.json, each command deckname hook --user …, every other entry kept), whatever a project declares. In every project the hook then:

  • serves a Read of a synced file as its covered fassung, and anonymizes Grep/Glob results whose search reaches a synced folder (no path: the session's folder);
  • denies Write, Edit, MultiEdit, NotebookEdit and shell writes into a synced folder;
  • denies a shell or Monitor command that names a synced folder (any spelling: quotes, ~, $VAR, %VAR%, $env:VAR, a wildcard, a folder above it searched recursively, or run from inside it) unless it only lists names;
  • blocks an @ mention of a synced file before the prompt is sent;
  • keeps a content index of each synced folder, so a copy of an original anywhere else is found by its text and anonymized.

An Akte's source on this computer is read through the Akte (read redirect, 17.10.2026). A file can lie in an Akte's source outside every sync folder: a workstation folder that is the source, a Git checkout of the source repository (the same remote in any spelling — https, ssh, git@host:, with or without .git — on the source's branch, below its folder), or a OneDrive/SharePoint folder Microsoft's client syncs here (found by the client's registry entries HKCU\Software\SyncEngines\Providers\OneDrive\*, MountPoint and UrlNamespace, against the source's web address). The hook asks the server where the person's Akten have their sources (GET /api/sync/source-places, kept for a minute in sync/source-places.json) and then:

  • answers a Read there with the Akte's released, covered version of that document — the same text a working copy is served (POST /api/sync/source-places/{group_id}/document by the path relative to the source's folder); no detection or review starts. A file the Akte has not released is refused with „Diese Datei gehört zur Akte „…“ und ist dort noch nicht freigegeben.“, one it does not hold with „… ist in der Akte nicht vorhanden.“ ([deckname:akte-source-read]);
  • refuses a write there ([deckname:akte-source-write]) and a shell command naming it unless it only lists names ([deckname:akte-source-shell]), handles a Grep/Glob reaching it as a read of a sync folder, and blocks an @ mention of a file there — the working copy's rules;
  • refuses every tool the server's own mirror of a Git source (<db folder>/repos/<Akte>.git) when the server runs on this computer ([deckname:akte-source-mirror]).

A Git checkout elsewhere on the disk is recognised per path (its .git), so a recursive search from a folder above it is only caught when the session's folder lies in it. A detached HEAD counts as the source's branch.

A regex escape (\|, \(, \*) or a quoted root pattern (grep -n "/*") is no path: a bare / or \ counts only as a whole word, and an escaped or quoted wildcard at a file system's root is not expanded (unless a nested shell, bash -c or eval, takes the string).

It also refuses credentials.json, sync/secrets.json and a .env holding DECKNAME_HOOK_AUTH_TOKEN to every tool, and withholds any tool output carrying one of their values. A project wired by declare apply answers its own events; the user-level call steps aside there. start says „Ordner geschützt“ only after it found the hook in place and the folder among the protected ones; otherwise „Ordner nicht geschützt“ with the settings file it could not write (--json: "protected").

status lists per group its source and every folder here: „Quelle“ (with „diese Workstation“ for a source on this computer) and each „Kopie“ with its state in the portal's words („Synchronisiert“, „Eingriff nötig“, …); under the first copy the group's folders, each with its documents and state („Verträge\2026\ 4 1 Eingriff nötig“, „leer“), a further copy „Ordner wie oben“. --json gives one row per folder with role, key, folder_id, source, folders and attention.

Ending the sync deletes the clear text, on request. stop takes the folder (or the group when it syncs here in one folder only). For a working copy it asks „Beenden und Dateien löschen“ or „Beenden, Dateien behalten“; without a terminal --wipe deletes the copy's folder (the trash included) and without it the files stay. A source asks „ ist die Quelle von „“. Trennen?“: the group keeps its documents and has no source afterwards, the working copies stay, and the source's files are never deleted (not with --wipe, not after a sign-off elsewhere); they stay protected while the folder exists. deckname logout asks the same for the groups of that server (--wipe without a terminal). Either way they are no longer synced: each one is signed off on the server. Kept files stay protected as above while the folder exists („Arbeitskopien bleiben auf diesem Rechner, werden aber nicht mehr synchronisiert.“). A computer signed off elsewhere (410) or a membership that ended (404, and the server no longer lists the group) loses the group's folder at once, with the notification „Akte auf dieser Workstation entfernt“.

Held instead of deleting. A pass sends no deletion when the group's folder is missing (moved, renamed, an unplugged drive), or when more than 20 files, or from 3 files on more than half of them, are missing at once. The group is paused, shows „Eingriff nötig“ in status with the reason (--json: "hold") and a notification. deckname sync resume "<group>" goes on once the folder is back; --allow-deletions confirms the deletions. A file whose bytes reappear under another name in the same pass is a rename and never counts toward this; in the folder tree it goes to the server as a move (a folder of the layout before still sends a deletion and a new document).

Working copies from the portal (Workstation). A sign-in in the browser (deckname login, not --token) and deckname setup (once the server offers the sync) make this computer the person's Workstation on that server: it is registered with its host name, system and client version (POST /api/sync/workstations), remembered in sync/workstation.json and the service is started with its autostart. In the portal the person then picks the Workstation and a folder („Auf Workstation synchronisieren“, the folder typed as text, ~ for the home folder) and the service sets the working copy up by itself: it takes the job at its start, on its Workstation's stream (GET /api/sync/workstations/{id}/events) or when the stream comes back after an outage, never on a timer. A Workstation that is switched off gets the job when it is back; a job waits at most 7 days. The folder must be a full path, outside a cloud client's folder (OneDrive, also %OneDrive%, Dropbox, Google Drive, iCloud Drive: refused, not only warned) and outside every folder synced here; then it is exactly sync start "<Akte>" "<Ordner>": made when missing, protected for the hook, registered and filled, its count reported to the portal while the first Run goes. Otherwise the job fails with a closed reason (folder_invalid, cloud_onedrive, folder_not_writable, folder_not_empty, folder_used, …) and nothing is set up. The portal also pauses, resumes and ends a working copy (ending deletes its files unless the person keeps them, which then stay protected) and signs the Workstation out: every working copy of that server ends the same way, a source stays (it is the person's own folder), and this computer forgets the Workstation until the next deckname login. Such a copy is an ordinary working copy here: status, pause, resume and stop treat it like any other. Nothing is ever asked: the service answers the portal, never the terminal. deckname logout signs the Workstation of that server out on the server first (POST /api/sync/workstations/{id}/sign-out, its waiting jobs are cancelled and it leaves „Meine Workstations“ at once), forgets it here and removes the service's autostart when nothing else keeps it (no folder synced, no Workstation of another server); the service then ends by itself. When the server does not answer, the logout still completes here and says that the portal entry stays: remove it there with „Ohne Löschen entfernen“. A revoked credential (logout, or „Entfernen“ of the token in the portal) also removes the Workstation that registered with it. The next deckname login registers this computer again, whatever signed it out, and withdraws a sign-out that still waited for it; a service that merely starts again does not bring a signed-out Workstation back.

A service that runs already picks up a new sign-in at once: it watches sync/workstation.json, follows the new Workstation's stream and looks at its jobs. A sign-in also replaces a service that runs other code than the installed client (an update, or an edited editable install): the service writes its version and a stamp of the package's files into sync/service.json, and deckname login asks an outdated one to end (sync/service.stop), ends it after 10 seconds if it does not, and starts the current one. A setup job counts the documents the server holds no original of (admitted before the originals' key, covered_only): they are not fetched, the job reports them as skipped (inside its done/total), the portal says „N Dokumente ohne Original, nicht geholt.“ and sync status shows them in the row „Nicht geholt“.

One server, one spelling. http://localhost:<port>, http://127.0.0.1:<port> and http://[::1]:<port> reach the same server, so the client keys them as one: credentials.json, sync/state.json and sync/workstation.json store every loopback host as 127.0.0.1 with its port. Files written before are merged when read (the newest credential of a deployment wins). Signed in or not is kept per server, so a stale copy of one spelling never flips the state of the other, and „Synchronisierung angehalten“ comes once per server, when it goes from signed in to signed out. Its action „Anmelden“ opens deckname login in a console window of its own.

The background service. One per user and computer for all folders, deckname sync run (started by the autostart, never by hand), one instance. It hears its folders' changes from the system and the server's from each folder's stream, and ends once no group is left, unless this computer is a Workstation: then it keeps running for the portal's jobs, and the last stop keeps the service and its autostart.

System Start now / autostart Removed by the last stop
Windows the venv's python.exe -m deckname_client.cli sync run, started with CREATE_NO_WINDOW (a console without a window); value deckname-sync under HKCU\Software\Microsoft\Windows\CurrentVersion\Run: the base install's pythonw.exe -I …\deckname_client\sync\windows_start.py with that command line the value is deleted
macOS ~/Library/LaunchAgents/deckname.sync.plist (RunAtLoad, restarted only after a failure), launchctl bootstrap gui/<uid> launchctl bootout, the file is deleted
Linux with systemd --user ~/.config/systemd/user/deckname-sync.service (Restart=on-failure), systemctl --user enable --now disable --now, the file is deleted
Linux without it a detached process; ~/.config/autostart/deckname-sync.desktop the file is deleted

--no-autostart starts only the process, until you sign out of the system. No admin rights are needed.

On Windows no console window opens, at the start and at logon. The venv's pythonw.exe and the deckname-syncw.exe gui script are not used for it: in a uv venv pythonw.exe is the same console launcher as python.exe, and started without a console it opened a black window whose closing ended the service. The Run value cannot pass process flags, so it names the base install's real pythonw.exe (no console) with windows_start.py, which starts the service with CREATE_NO_WINDOW. Every sign-in writes the value again, so an older one moves over. The link deckname://sync/<group_id> is registered on the first start on Windows (HKCU\Software\Classes\deckname) and Linux (a .desktop handler for x-scheme-handler/deckname); macOS has none (the web offers „Befehl kopieren" there).

What stays on this computer, beside credentials.json in the client's per-user state directory and never in a project: sync/state.json (per synced folder the group, the server, the device id, the folder, its role and folder id, the group's folders, the cursor, per document its fassung, hash and path, what waits in review, and your open reviews and overlaps with their links — never a token; and the folder id of every folder that ever synced here), sync/sync.log (one JSON line per file written, removed, put back or sent, in the launch log's six columns: a file name, never a path or any text; status -v shows the last 20), sync/service.log (the service's own Log, one line per start with pid, version and servers, per contact, job taken or ended with its counts, stop and error; servers, job kinds and counts, never a document's text; rotated past 256 KB into service.log.1), sync/service.json (the running service's pid, start and code) and the locks. On Windows the folder is %LOCALAPPDATA%\deckname-client\sync.

deckname sync — exit codes and stderr markers (a published contract)

Like prepare's table: CI branches on the code, alert routing greps the marker. On a terminal a failure is a status block on stderr („Nicht gestartet: … / Behebung: … / Geprüft: …") instead of the marker; the code is the same. --json names the error instead.

Exit error Meaning stderr marker
0 — done, also „läuft bereits" (silent)
1 confirm_required cancelled (Strg+C or „Abbrechen", without a marker), or stop without --yes and no terminal to ask [deckname:sync-confirm-required]
2 not_in_release the server's release has no sync: „In dieser Version nicht verfügbar." — argparse's usage error keeps the same number, without a marker [deckname:sync-not-in-release]
4 not_signed_in no deckname login for the server (or it was refused), or several and no --server [deckname:sync-not-signed-in]
5 server_unavailable the server could not be reached, or answered unreadably [deckname:sync-server-unavailable]
6 group_unknown no such group among yours — the same answer for a foreign and a missing one; for --quelle also a group that has a source (reason: source_exists), is not empty, where you only read, or a new group's name that is taken; for --vorgang a code that is unknown, not yours, closed or expired (reason: vorgang_gone), or --vorgang without --quelle; for stop a group synced in several folders here (name the folder) [deckname:sync-group-unknown]
7 too_large --quelle: the folder holds more files or bytes than one Akte's source may (the server's limits); nothing is registered [deckname:sync-too-large]
8 folder_unusable the folder is not writable, holds files that are not this group's, lies inside or around another sync folder, or (for --quelle) is missing, a drive or the home folder, or the source of another group already [deckname:sync-folder-unusable]
9 service_failed the background service could not be set up (detail: the system's message); nothing stays registered [deckname:sync-service-failed]

3 stays free (login uses it). 7 once meant "local sync switched off", which the group sync no longer has; since „Quelle zuerst“ (16.10.2026) it is too_large.

login / logout — a workstation stops being nobody

deckname login signs THIS workstation in to a deployment. It opens a sign-in session on the server and the browser at the portal page /login/cli/<session>, which names this machine (hostname, OS, how long ago). There the person signs in if needed, presses „Anmelden“ and gets a short code. The terminal asks „Code aus dem Portal eingeben:“, redeems the code and prints „Angemeldet als auf .“. The code is bound to a verifier only this process holds (PKCE-like), works once, expires after five minutes and allows five attempts. The credential is stored outside every workspace (per-user, per-machine state, never .env and never anything a checkout commits) and always acts as role user, never admin. Once signed in, deckname login --status names who and where, without a new sign-in; deckname logout signs this computer's Workstation out and revokes the credential on the SERVER first, then removes it locally (synced folders are no longer synced, see sync above). A sign-in in the browser also makes this computer the person's Workstation on that deployment and starts the sync service, so the portal can set up working copies here (see sync above); --token does not.

deckname login                    # opens the portal page, asks for the code it shows
deckname login --status           # names the current account and deployment
deckname login --code             # no local browser: prints the address to open elsewhere, then asks for the code
deckname login --token <value>    # a credential already issued elsewhere (headless / CI)
deckname login --deployment <url> # target a deployment other than hook_server_url
deckname logout

This credential is separate from DECKNAME_HOOK_AUTH_TOKEN: that one is the workstation's own static bearer for the hook command's /api/anonymize calls (see "declare — generate the hook wiring instead of hand-editing it" above), shared by everyone who checks out the same project. The login credential says who is signing in, not which machine may call the anonymizer, and one workstation may hold one for each of several deployments at once without either overwriting the other.

Behaviour change: a gated deployment now refuses an unauthenticated hook read

manual/four-eyes deployments used to park every hook-sourced review under the deployment's own implicit identity, so a workstation that had never run deckname login still got a protected file reviewed and delivered — under an owner label no real person could ever hold, and one that a four-eyes deployment could never exclude as its own approver. That stops on upgrade. A document_source read (the hook command's protected-file path) now carries this workstation's own login credential as an additive header, and a deployment that gates reads (manual/four-eyes) refuses to park a review it cannot attribute to a real, logged-in person.

What that means in practice: on a gated deployment, a workstation that has never run deckname login gets every protected read withheld — decision: "block", plus an operator-facing message naming the fix on the terminal. Run deckname login on that workstation and the read is restored on the next attempt; nothing else about the declaration or the hook wiring changes. An auto deployment is unaffected either way — nothing there was ever gated, and this change touches only the gate.

Behaviour change: a stale .claude/settings.json withholds instead of leaking

declare apply derives the host timeout it writes into .claude/settings.json from the review mode the deployment PUBLISHED at the moment it ran — 8 s under auto, 305 s under manual, because a parked read waits on a person. Nothing re-runs declare apply when that published word later changes, and a deployment upgrading to the current server picks up manual as its default.

So a workstation that applied against an auto deployment carries a host timeout far too short for a review wait. Left alone, the hook would engage the gate, wait its 300 s client deadline, be killed by the host at 8 s with empty stdout — and Claude Code would deliver the ORIGINAL, un-redacted file content to the model. That is now detected and refused. declare apply records the deadline it derived (DECKNAME_HOOK_HOST_REVIEW_TIMEOUT_SECONDS in the generated env block); the hook compares it against the wait it is about to start and, when it does not cover it — or when the key is absent, i.e. a settings file generated before this change — withholds the read with decision: "block" and an operator-facing message.

What that means in practice: after upgrading a deployment to a gating review mode, run deckname declare apply once on each workstation. deckname declare check reports the drift beforehand without changing anything. An auto deployment is unaffected: no review engages, so the check never runs.

Behaviour change: on four-eyes, the person who caused a hook review can no longer approve it alone

A hook-sourced review is now parked under the logged-in workstation's own account instead of the deployment's shared implicit identity — this is what the previous section's attribution fix buys, beyond the block/restore behaviour itself. On a four-eyes deployment the existing two-distinct-approvers rule follows that identity: the person whose workstation caused a protected read is excluded from casting the second, releasing approval on that same review. Previously the owner label was an implicit identity no real approver could ever match, so that exclusion never applied to a hook-sourced review and the requester could countersign their own read. It now behaves exactly like a review opened through the browser or an MCP client — the requester may still cast the FIRST of the two required approvals (the ordinary elicit flow), but a colleague's distinct approval is required to release it.

Behaviour change: MCP-parked and egress reviews get a personal owner too

The two sections above cover the hook's own document_source read. The same rule now closes the gap on the two doors that were left behind: an anonymize call that gates, submit_for_review called over MCP, and the PreToolUse egress vet that parks the cleartext-exception review for a call the hook denied outbound.

On upgrade: when this workstation is signed in (deckname login) for the deployment AND the session was started through deckname launch, a review parked through either MCP tool, or through a denied outbound call, is now owned by the logged-in person — the same way a hook-sourced review already is — and shows up in that person's own „Meine Prüfungen" list instead of an implicit identity nobody can claim. As on the hook path, this changes ownership only: which config applies, who may call which tool, and the response shape of every call are all unchanged (## Not this in the milestone: identity is for attribution here, never for policy or authorization).

Two conditions, and what happens short of them. A workstation with no deckname login credential for the deployment, or a session started with claude directly rather than through deckname launch, is a supported state, not a failure: the review still parks, exactly as it always has, under the workstation's own implicit identity — not every MCP or egress review becomes personally owned on upgrade, only the ones a logged-in, launcher-started session parks. deckname selftest reports which case this workstation is in and names deckname login when that is what is missing.

On a four-eyes deployment, the person whose outbound call was denied can no longer cast the releasing approval of their own cleartext exception. Before this milestone the owner of that review was an implicit identity no real approver could ever match, so the requester's own approval alone could release their own exception. It now behaves like every other personally-owned review: the requester may still cast the first approval, but a colleague's distinct approval is required to release it.

Group documents in Claude Code — the group-documents skill

The server's MCP entry in .mcp.json brings four tools for the documents of the groups the signed-in person belongs to. Every one of them returns covered (anonymized) text only, and a group the caller holds no role in is refused exactly like an unknown one:

Tool Returns
list_groups the caller's groups, with role and document count
list_group_documents one group's documents, newest first: covered titles, no text (at most 200, truncated beyond)
search_group_documents matching covered passages in one group (mode: "lexical" or "hybrid")
get_group_document one document's full covered text

Next to that entry, declare apply — and so deckname setup and the bare deckname run, which call it — writes .claude/skills/group-documents/SKILL.md: a fixed English text that tells the model when to look in the group documents (team documents, templates, contracts, policies), to search with a few distinctive content words and retry with synonyms (the search is lexical unless the answer says hybrid; under hybrid it also matches by meaning, so a short phrase of what the user wants may follow), to cite title and group, never to guess what a placeholder such as <PERSON_1> stands for, and to take the membership refusal at its word. It is the same text in every workspace: no group name, title, path or account is written into it. The file exists while the registration carries a credential (a deckname login credential or a static DECKNAME_HOOK_AUTH_TOKEN), since the group tools refuse every other caller; the next declare apply after the last credential is gone removes it. Like protected-sources, it is advice to the model, not a barrier — the server decides who reads what.

Group sync in Claude Code — the group-sync skill

Right after group-documents, declare apply (and so deckname setup and the bare deckname run) writes .claude/skills/group-sync/SKILL.md: a fixed English text that tells the model how to use deckname sync on the user's behalf — always with --json, since its shell is no terminal; status whenever the user asks about the sync; start, pause, resume and stop only when the user asked for exactly that, with the folder the user named; never deckname sync run (the service's own) and never deckname login (the user confirms that in a browser); and that it cannot approve a review. It explains every state and every exit code with its error (the table above). It is the same text in every workspace: no group name, folder, device or account is written into it — deckname sync status --json answers those live.

The file follows the registration and the server's release, asked once per run: it is written while the registration carries a credential (as for group-documents) and the server names sync among its release features; removed (with its folder, once empty) when the server answers without sync or the credential is gone; and left exactly as it is when the server does not answer. A workspace with no credential and no such file asks nothing. deckname sync itself writes no skill — it is not run per workspace. Since 2026-10-15 it explains sources and working copies, start --quelle and stop by folder, and that a folder changed in a sync folder changes the group too.

Metadata

Release files for deckname-client 1.0.0rc1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for deckname-client 1.0.0rc1
File Size Uploaded Provenance
deckname_client-1.0.0rc1.tar.gz 767.8 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for deckname-client 1.0.0rc1
File Interpreter ABI Platform
deckname_client-1.0.0rc1-py3-none-any.whl Python 3 none any Details

Total release size: 1.6 MB

Release files / deckname_client-1.0.0rc1.tar.gz

Download URL deckname_client-1.0.0rc1.tar.gz
Size 767.8 kB
Tags Source
SHA-256 checksum
How to use checksums
a7fc89e09395d1c3f0e577d09629479fdcfcc5c5b2e7ebf0a9f92c66f2a45b3a
BLAKE2b-256 checksum
How to use checksums
0bc37deed1ebcaf002878952273fabc9f95a15e58a73d4a9e2654bc6c3bab9ae
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.

Transparency log

Release files / deckname_client-1.0.0rc1-py3-none-any.whl

Download URL deckname_client-1.0.0rc1-py3-none-any.whl
Size 853.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
718ff7df371c4e5c27281b8468922c44632c1ca6932071108455c64950edb83c
BLAKE2b-256 checksum
How to use checksums
a84e308f63dd432b2ed2ec3f323fc5c4b5b35bca6a102f527fa43891cf6c7809
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.

Transparency log

Security for deckname-client 1.0.0rc1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for deckname_client-1.0.0rc1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

1.0.0rc1 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page