<!-- Generated by Vibe Doc v0.3.2 -->
<!-- Date: 2026-04-17T00:39:25.345Z -->
<!-- Classification: IntegrationConnector -->
<!-- Source artifacts: 69 files scanned -->
<!-- Confidence: 1 high, 0 medium, 0 low sections -->

---
docType: threat-model
version: 1.0
templateVersion: 1
---
<!--
Generated: 2026-04-17T00:39:25.344Z
Classification: IntegrationConnector
Classification rationale: Client-side desktop widget. No backend. Reads claude.ai REST API
on behalf of a signed-in user, stores session credentials locally in Windows Credential Manager.
Source artifacts: 69
-->

# Threat Model — Sanduhr für Claude (Windows v2.0)

## Overview

Sanduhr für Claude is a frameless PySide6 desktop widget that reads the authenticated user's
Claude.ai usage quota and renders it as a sparkline overlay. It is strictly client-side:
there is no backend, no telemetry endpoint, and no cloud storage. All network traffic goes
directly between the user's machine and `claude.ai`.

**Version covered:** Windows v2.0.0 (`windows-native` branch)  
**Date:** 2026-04-17  
**Authors:** Este + Stitch  

---

## Asset Scope

### Critical Assets

| Asset | Location | Sensitivity |
|---|---|---|
| `sessionKey` cookie | Windows Credential Manager — service `com.626labs.sanduhr`, account `sessionKey` | **High** — long-lived auth token granting access to the user's Claude.ai account |
| `cf_clearance` cookie (optional) | Windows Credential Manager — service `com.626labs.sanduhr`, account `cf_clearance` | **Medium** — short-lived Cloudflare bypass token; loss of confidentiality allows session impersonation on Cloudflare-blocked networks |
| Installed binary | `C:\Program Files\Sanduhr\` (or per-user `%LOCALAPPDATA%\Programs\Sanduhr\`) | **Medium** — tampering could execute malicious code under the user's account |

### Low-Sensitivity Assets

| Asset | Location | Sensitivity |
|---|---|---|
| Sparkline history | `%APPDATA%\Sanduhr\history.json` | Low — usage percentages over time, no PII |
| Settings | `%APPDATA%\Sanduhr\settings.json` | Low — theme preference, window position |
| Application log | `%APPDATA%\Sanduhr\sanduhr.log` (rotating, 1 MB × 3) | Low — operational events; credential values are never written (see §Mitigations) |

### Entry Points

- User pastes `sessionKey` (and optionally `cf_clearance`) into the in-app credentials dialog.
- Application reads stored credentials from Windows Credential Manager at startup and on each refresh cycle.
- HTTP GET requests to `https://claude.ai/api/organizations` and `https://claude.ai/api/organizations/{org_id}/usage`, with a 15-second timeout.
- Installer `Sanduhr-Setup-v2.0.0.exe` (unsigned) downloaded from GitHub Releases.
- Optional autostart via `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` (opt-in, unchecked by default in installer).

<!-- Source: api.py (_API_BASE, _get, timeout=15), credentials.py (SERVICE, _ACCOUNT_SESSION, _ACCOUNT_CF), Sanduhr.iss ([Registry] autostart task) -->

---

## Threat Actors

### In Scope

| Actor | Motivation | Capability |
|---|---|---|
| Malware / other processes running as the same Windows user | Steal `sessionKey` to access Claude.ai account, read conversation history, or consume quota | Any process running as the same user can query Windows Credential Manager entries for that user. This is a standard Windows trust boundary, not a Sanduhr-specific weakness. |
| Attacker who has already compromised the user's Windows session | Same as above | Full access to Credential Manager, filesystem, and running processes |
| Supply chain attacker (dependency tampering) | Execute arbitrary code in Sanduhr's process | Ability to publish a malicious version of a pinned dependency to PyPI |
| Binary tampering / installer substitution | Execute arbitrary code when user installs or runs Sanduhr | Ability to serve a malicious installer from GitHub Releases or a man-in-the-middle position |

### Out of Scope

| Actor | Reason |
|---|---|
| Remote attacker with no foothold on the user's machine | No server-side component, no inbound network port, no attack surface reachable without local access |
| Anthropic / Claude.ai infrastructure compromise | Out of application control; Sanduhr is a passive consumer of the API |
| Physical access attacker | Standard OS threat model; beyond application scope |

<!-- Source: design spec non-goals (client-only architecture); credentials.py (keyring service name); Sanduhr.iss (PrivilegesRequired=lowest — user-space install) -->

---

## Threat Scenarios

### T-01 · Credential exfiltration via malware on the user's machine

**Asset targeted:** `sessionKey`, `cf_clearance`  
**Actor:** Malware or another process running as the same Windows user  
**Description:** Any process running under the same user account can call
`cmdkey` or the Windows Credential Manager API and read the stored `sessionKey`.
This is inherent to how Windows Credential Manager works — it is not a
Sanduhr-specific weakness — but it is the highest-impact credential risk.  
**Impact if exploited:** Attacker gains full Claude.ai session access for the
token's validity period.  
**Scope decision:** **Out of scope for Sanduhr to fully mitigate.** Sanduhr is
not a sandboxed product and makes no claim to protect credentials from malware
with the same user-level privilege. The relevant control is OS-level
(keeping the machine clean of malware).

<!-- Source: credentials.py (keyring.get_password — no additional ACL hardening beyond Credential Manager defaults) -->

---

### T-02 · Credential leak via application log

**Asset targeted:** `sessionKey`, `cf_clearance`  
**Actor:** Any party who can read `%APPDATA%\Sanduhr\sanduhr.log` (same user, or
attacker with file read access)  
**Description:** If credential values were written to the rotating log file, an
attacker with log-read access (but not Credential Manager access) could harvest
them.  
**Impact if exploited:** Same as T-01.  
**Mitigation in place:** `credentials.py` never logs credential values. The
`save()`, `load()`, and `migrate_from_v1()` functions log only structural events
(`"v1 -> v2 migration complete: %s", result`) where `result` is a boolean dict
(`{"migrated": True, "session_key": True, …}`), not the actual values. The
`fetcher.py` error path (`_log.exception("Unexpected fetch failure")`) logs
exception text from `api.py` exception messages, which contain HTTP status codes
but never credential values (exception messages are constructed in `_check()`
from status codes only).  
**Residual risk:** Low. No evidence of accidental credential logging found in
reviewed source files.

<!-- Source: credentials.py (lines 66, 96 — _log.info logs boolean result dict, not values); api.py (_check, SessionExpired, NetworkError — messages contain HTTP codes, not cookies); fetcher.py (line 63 — _log.exception logs exception type/message only) -->

---

### T-03 · v1 plaintext credential exposure window

**Asset targeted:** `sessionKey` (v1 stored plaintext at `~/.claude-usage-widget/config.json`)  
**Actor:** Malware or process that reads the filesystem  
**Description:** Before v2 first-run migration completes, the v1 config file
(`~/.claude-usage-widget/config.json`) contains the `sessionKey` in plaintext.
Any process with file-read access can harvest it during this window.  
**Impact if exploited:** Same as T-01.  
**Mitigation in place:** `credentials.py:migrate_from_v1()` reads the v1 file,
moves the `sessionKey` to Credential Manager, and deletes the source file
(`legacy.unlink()`) in the same function call. The plaintext file is removed on
first v2 launch.  
**Residual risk:** The exposure window is the duration of the first v2 launch
(seconds). If the migration crashes before `legacy.unlink()` is reached — for
example, due to an `OSError` on the `unlink` call — the plaintext file persists
and a warning is logged (`"Could not delete v1 config: %s"`). In that case the
user retains a plaintext credential file until they manually delete it.  
**Gap:** There is no retry or follow-up deletion attempt if `unlink` fails. If
`unlink` raises `OSError`, the migration result still reports `migrated: True`
even though the source file was not removed.

<!-- Source: credentials.py (migrate_from_v1, lines 91–94 — unlink wrapped in try/except, failure only logged as warning) -->

---

### T-04 · Cloudflare bypass via `cloudscraper`

**Asset targeted:** Cloudflare's bot-detection signal; indirectly the user's
Claude.ai account  
**Actor:** Anthropic / Cloudflare policy enforcement  
**Description:** `api.py` uses `cloudscraper` to pass Cloudflare challenges on
`claude.ai`. This mimics browser TLS fingerprints and JavaScript challenge
responses to obtain a `cf_clearance` cookie.  
**Trust level:** Sanduhr is acting on behalf of the authenticated user reading
their own usage data. The trust level is equivalent to the user opening
`claude.ai` in a browser. The `sessionKey` is the same credential a logged-in
browser session uses.  
**Potential concern:** If Anthropic's terms of service prohibit programmatic
access to their usage API, use of `cloudscraper` heightens that violation by
bypassing their Cloudflare layer.  
**Impact if enforced:** Anthropic could invalidate the `sessionKey`, block the
IP, or change the API contract. This is a policy risk, not a security risk to
the user.  
**Scope decision:** Accepted as-is for v2.0. Sanduhr reads only usage quota data
and does not perform any write operations or conversation access.

<!-- Source: api.py (cloudscraper.create_scraper, _cookie_header, _looks_like_cloudflare); TEST_PLAN.md §7 Cloudflare block -->

---

### T-05 · Supply chain attack via dependency tampering

**Asset targeted:** User's machine (code execution), `sessionKey` in memory  
**Actor:** Attacker who can publish to PyPI or compromise a dependency maintainer  
**Description:** A malicious version of `PySide6`, `cloudscraper`, `keyring`, or
`requests` published to PyPI could execute arbitrary code in Sanduhr's process,
where it would have access to in-memory credentials.  
**Mitigation in place:** All four dependencies are pinned to exact versions in
`requirements.txt`:
- `PySide6==6.8.0.2`
- `cloudscraper==1.2.71`
- `keyring==25.5.0`
- `requests==2.32.3`

The PyInstaller-bundled `dist/` directory bakes these pinned versions into the
installer at build time — end users do not run `pip install` and are not exposed
to live PyPI.  
**Gap:** Pin integrity is not verified by a hash lockfile (`pip-audit`,
`pip-compile --generate-hashes`). An attacker who replaces a PyPI package at the
pinned version (version-squatting attack) would affect the next build but not
existing installed binaries.

<!-- Source: windows/requirements.txt (exact pins); design spec architecture (PyInstaller bundling) -->

---

### T-06 · Unsigned binary execution / installer substitution

**Asset targeted:** User's machine  
**Actor:** Attacker who can substitute the installer at the download source
(GitHub Releases) or intercept the download  
**Description:** The installer `Sanduhr-Setup-v2.0.0.exe` is not code-signed.
Windows SmartScreen shows a "Windows protected your PC — Unknown publisher"
warning. Users must click "More info" → "Run anyway." There is no cryptographic
guarantee that the binary a user downloads was built from the reviewed source.  
**Impact if exploited:** Arbitrary code execution on the user's machine, with
all user-level privileges.  
**Mitigation in place:** None cryptographic. Distribution is via GitHub Releases
on a private/semi-public repo. SmartScreen warning is the only prompt.
`RELEASE_NOTES.md` documents the "Run anyway" click as expected behavior.  
**Deliberate deferral:** Code signing is explicitly a non-goal for v2.0 per the
design spec: *"Not code-signed at v2.0. SmartScreen warning is acceptable for a
side-project release. Signing infrastructure can be added in a two-line build
step later."*  
**Gap:** This is a known, accepted gap. Any future public release should add
code signing.

<!-- Source: RELEASE_NOTES.md §Install step 2; design spec Non-goals "Not code-signed at v2.0"; Sanduhr.iss [Setup] — no SignTool directive present -->

---

### T-07 · Credential Manager entries surviving uninstall

**Asset targeted:** `sessionKey`, `cf_clearance` on a shared or reused machine  
**Actor:** Next user of the same Windows user account after Sanduhr is uninstalled  
**Description:** If Credential Manager entries are not cleaned up on uninstall,
stored credentials persist indefinitely. On a shared machine or after account
handoff, this is a meaningful risk.  
**Mitigation in place:** `Sanduhr.iss [UninstallRun]` executes
`cmdkey.exe /delete:com.626labs.sanduhr` as a hidden post-uninstall step,
unconditionally (not gated on the "Also remove my settings and history"
checkbox). `TEST_PLAN.md §9` verifies this in both checkbox states.  
**Residual risk:** `cmdkey.exe /delete` is best-effort; the `.iss` marks it with
`Flags: runhidden` and `RunOnceId: "ClearCreds"` but does not surface failure to
the user. If the `cmdkey` call fails silently (e.g., the entry was already
deleted), this is benign. If it fails to delete a real entry (e.g., permission
error in an unusual configuration), credentials would persist.

<!-- Source: Sanduhr.iss [UninstallRun] lines 60–61; TEST_PLAN.md §9 Uninstall -->

---

### T-08 · Installer privilege escalation

**Asset targeted:** System-wide files (if installer ran elevated)  
**Actor:** Installer itself, or an attacker exploiting an elevated installer  
**Description:** If the installer ran as Administrator, a compromised or
substituted installer would have system-wide write access.  
**Mitigation in place:** `Sanduhr.iss [Setup]` sets `PrivilegesRequired=lowest`
with `PrivilegesRequiredOverridesAllowed=dialog`. The installer defaults to
user-space install (`{autopf}` resolves to `%LOCALAPPDATA%\Programs` for
non-admin users). Users can opt to elevate for a system-wide install, but this
is not the default.  
**Residual risk:** If a user chooses to run the installer elevated (system-wide
install), the risk profile of T-06 (unsigned binary) is amplified — a tampered
installer would write malicious files to `C:\Program Files\Sanduhr\` with
admin-level trust.

<!-- Source: Sanduhr.iss [Setup] PrivilegesRequired=lowest, PrivilegesRequiredOverridesAllowed=dialog, DefaultDirName={autopf} -->

---

### T-09 · Log file disclosure

**Asset targeted:** Operational data (API response structure, error codes,
exception stack traces)  
**Actor:** Any process or user with read access to `%APPDATA%\Sanduhr\sanduhr.log`  
**Description:** The rotating log captures DEBUG-level output when launched with
`--debug`, including API HTTP response codes, exception messages, and migration
events. Stack traces from unhandled exceptions are captured via `sys.excepthook`.  
**Impact:** Low. Log content does not include credential values (see T-02). Could
reveal API endpoint paths or response structure to a local attacker — this is
already public information.  
**Residual risk:** Low.

<!-- Source: app.py (_configure_logging — RotatingFileHandler, maxBytes=1_000_000, backupCount=3; DEBUG level gated on --debug flag; sys.excepthook logs unhandled exceptions) -->

---

## Mitigations

### In-Place Controls Summary

| Control | Addresses | Evidence |
|---|---|---|
| Windows Credential Manager storage via `keyring` | T-01, T-02, T-03, T-07 | `credentials.py` — `keyring.set_password` / `get_password` with service `com.626labs.sanduhr` |
| Credentials never logged (value suppressed) | T-02 | `credentials.py` — `_log.info` logs boolean result dict only; `api.py` exceptions contain HTTP codes only |
| v1 plaintext config deleted on first v2 run | T-03 | `credentials.py:migrate_from_v1()` — `legacy.unlink()` called after keyring write |
| Pinned dependency versions, bundled via PyInstaller | T-05 | `windows/requirements.txt` — exact version pins; PyInstaller bundles at build time |
| User-space install by default (lowest privilege) | T-08 | `Sanduhr.iss` — `PrivilegesRequired=lowest` |
| Credential Manager cleanup on uninstall | T-07 | `Sanduhr.iss [UninstallRun]` — `cmdkey.exe /delete:com.626labs.sanduhr` |
| 15-second API request timeout | T-04 (partial) | `api.py:_get()` — `timeout=15` |
| API access read-only (GET only) | T-04 | `api.py` — only `_get()` implemented; no POST/PUT/DELETE |

<!-- Source: credentials.py, api.py, windows/requirements.txt, Sanduhr.iss -->

### Known Gaps (No Mitigation in Place)

| Gap | Threat | Notes |
|---|---|---|
| No code signing | T-06 | Deliberate deferral per design spec non-goals. SmartScreen warning only. |
| No dependency hash lockfile | T-05 | Versions are pinned but not hash-verified at build time |
| `unlink` failure in v1 migration not retried | T-03 | If `OSError` on `legacy.unlink()`, plaintext credential file persists; only a warning is logged |
| No HTTPS certificate pinning | T-04 | Relies on OS/system trust store for `claude.ai` TLS verification; standard for desktop apps |

---

## Residual Risks

### R-01 · Same-user process access to Credential Manager
Any process running as the authenticated Windows user can read the stored
`sessionKey`. This is inherent to the Windows security model and is out of
scope for Sanduhr to address. Accepted for v2.0.

### R-02 · v1 migration `unlink` failure
If `legacy.unlink()` raises `OSError` during v1 migration, the plaintext
`~/.claude-usage-widget/config.json` is not removed and a warning is logged.
The user would need to notice the warning or manually delete the file.
Low likelihood (only affects users upgrading from v1); low-medium impact.

### R-03 · Unsigned installer
No cryptographic guarantee that the downloaded installer is untampered.
Accepted for v2.0 per the non-goals in the design spec. Risk increases if
distribution moves from a private GitHub repo to a public download page.

### R-04 · Supply chain (build-time only)
Pinned versions protect installed users. A compromised PyPI package at the
pinned version number would affect the next build. Risk window: between a
supply-chain compromise and the next build of a release.

### Severity × Likelihood ratings (owner-confirmed 2026-04-16)

| ID   | Threat                                              | Severity  | Likelihood |
| ---- | --------------------------------------------------- | --------- | ---------- |
| T-01 | Same-user process reads Credential Manager entry    | Medium    | Low        |
| T-02 | Credential leak via log file                        | High      | Very Low   |
| T-03 | v1 plaintext config exposure window during migration | Medium   | Low        |
| T-04 | Cloudflare bypass — policy risk                     | Low       | Low        |
| T-05 | Supply chain via PyPI dependency tampering          | High      | Low        |
| T-06 | Unsigned binary / installer substitution            | Medium    | Low        |
| T-07 | Uninstall doesn't clean Credential Manager entries  | Low       | Very Low   |
| T-08 | Installer privilege escalation                      | Medium    | Very Low   |
| T-09 | Log file disclosure                                 | Low       | Low        |

**Risk tolerance posture:** this is a side-project tool used by individuals,
not an enterprise product. Residual risk is considered acceptable when
(a) no credentials leak into logs or plaintext on disk, (b) the trust
boundary is explicitly the user's Windows account, and (c) every
deliberate tradeoff (unsigned binary, Cloudflare bypass, no compliance
framework) is documented here and in the design spec's non-goals section.
<!-- Source: user confirmation during vibe-doc interview 2026-04-16 -->

---

## Out-of-Scope Items

These are named here so readers understand the boundary, not because Sanduhr
claims to address them.

- **Malware with same-user access** — any application running as the same user
  can access Credential Manager. Protecting credentials from same-privilege
  malware would require OS-level sandboxing (e.g., AppContainer) that is not
  implemented and not planned.
- **Anthropic backend compromise** — Sanduhr is a passive consumer of the
  claude.ai API. A compromise of Anthropic's infrastructure is outside
  application scope.
- **Network-level MITM on claude.ai** — standard TLS; no additional certificate
  pinning. Any desktop app is in the same position.
- **Physical access attacks** — standard OS threat model.

---

## Confirmed Decisions (2026-04-16 owner interview)

The following were confirmed during the vibe-doc interview on 2026-04-16.
They represent the owner's current posture; revisit on any distribution or
scope change.

### Compliance

No compliance frameworks apply (SOC 2, GDPR, HIPAA, PCI, etc.). Sanduhr is
a personal tool with a single-user install, and no data leaves the user's
machine except what they already send to `claude.ai` in a browser.

### External review

No external security review has occurred — solo audit only. If contributors
join or distribution expands materially, revisit.

### Incident response — `sessionKey` compromise

If a session key is believed compromised (accidental commit, leaked
screenshot, device theft), follow this drill:

1. **`claude.ai` → change account password** — typically invalidates all
   active sessions and forces re-auth everywhere.
2. **Explicit sign-out** on every device and browser where the user has been
   signed in to `claude.ai`.
3. **Sanduhr-local cleanup.** Easiest: uninstall with "Also remove my
   settings and history" checked. Alternative: Start → Credential Manager →
   delete entries under service `com.626labs.sanduhr` (accounts `sessionKey`
   and `cf_clearance` if present).
4. **Check `claude.ai` activity** — chat history and usage dashboard — for
   unexpected activity during the exposure window.
5. **Only after the above**, reinstall Sanduhr and paste a fresh
   `sessionKey`.

### Code signing timeline

Signing is deferred until one of these triggers:

- User complaints about SmartScreen become frequent
- Project targets listing on Microsoft Store (signing is a prerequisite)
- Public launch or press beyond the current audience
- First downloaded by anyone not on a personal allowlist

Until a trigger hits, SmartScreen "unknown publisher" warning is accepted
friction and is documented in `RELEASE_NOTES.md` + `TEST_PLAN.md`.

### Distribution expansion posture

Current distribution: GitHub Releases only. **Expansion to any other
channel happens only after signing is in place** — this ties back to the
signing triggers above. Moving to winget / Chocolatey / Scoop / a public
download page / MS Store without signing would materially raise T-06
(unsigned binary / installer substitution) risk, which the owner is not
willing to accept.

<!-- Source: user confirmation during vibe-doc interview 2026-04-16 -->
