Dotfiles improvement report

Audit date: 2026-08-29 · revision: af93e69 (main, aligned with origin/main) · scope: current working tree

Executive summary

The repository has a good small-system foundation: plain-text package manifests, a dry-run linker, XDG-aware helpers, checksum verification for several downloads, and clear separation between setup, configuration, personal commands, services, and automation. The tracked source is only about 559 KB across 200 files.

The most important problem is not missing features; it is an unclear boundary between source, machine state, and secrets. link.sh walks the checkout filesystem rather than the Git manifest, while the working tree currently contains generated state, credentials, dependency trees, session exports, temporary repositories, and required-but-untracked source. That makes a fresh clone differ from the running machine and allows accidental files to become live home configuration.

The recommended sequence is:

  1. Rotate leaked credentials and reduce runtime privilege.
  2. Make Git-tracked files the deployment boundary.
  3. Restore a clean, reproducible checkout.
  4. Add a small validation suite and CI.
  5. Harden scheduled jobs and only then improve portability or add profiles.

Do not start with a new dotfile manager, framework, or rewrite. The current design can be made reliable with a few explicit contracts and small tests.

Current state

Measure Observed
Tracked files 200
Tracked lines 16,535
Tracked bytes 558,794
Executable tracked files 92
Current changes 34 modified, 4 deleted, 43 untracked entries
Checkout disk use excluding .git 295 MB
Matrix bridge node_modules 196 MB
Jujutsu state (.jj) 88 MB
Untracked Pi session export 3.4 MB
Automated project tests None collected

The size discrepancy is useful evidence: the repository source is small, but its directory has become a workspace and runtime-state container.

Git-tracked source -> explicit installer/linker -> ~/.config, ~/.local/bin
                         |                         |
                         v                         v
                  secret store             XDG runtime state
                  never committed          never linked from checkout

What is already working well

Priority 0 — act before normal cleanup

1. Rotate credentials exposed in reachable Git history

A deleted FreshRSS extension in commit af6e2ce contains a hard-coded credential, and commit b2d92e9 contains Pi OAuth credential fields in a conflict artifact. Deleting the working-tree files did not remove the values from history.

Action

  1. Revoke/rotate the FreshRSS and Pi OAuth credentials immediately.
  2. Use git filter-repo to remove the affected paths from all refs.
  3. Force-push the rewritten history and expire local reflogs only after confirming the replacement credentials work.
  4. Ask anyone with a clone to reclone or purge the old objects.
  5. Run a history-aware secret scanner in CI. It should report path and commit, never print secret values in logs.

Acceptance test: scanning every reachable object reports no credential material; old credentials fail authentication.

2. Reduce Pi container and Matrix bridge authority

The Pi container mounts the entire home read-only, its workspace and Pi state read-write, AWS configuration, and the host Podman API socket (.local/bin/pi.sh:138-157). A read-only Unix socket mount does not make API operations read-only. The Matrix bridge can enumerate/open Pi sessions, select arbitrary directories, and submit commands or prompts (.local/src/matrix-pi-bridge/bridge.ts:226-289, 380-427).

This means prompt injection or a compromised Matrix account can approach full user-level compromise.

Smallest useful change

  • Remove the Podman socket from the default Pi invocation; add it only to an explicit container-development command.
  • Mount the current repository/worktree, not all of $HOME.
  • Provide AWS credentials only to tasks that need AWS.
  • Restrict the Matrix bridge to one dedicated workspace and a small explicit command/tool set.
  • Document that the bridge is a privileged remote-control channel, not merely chat.

Acceptance test: the default container cannot list host containers, read unrelated home files, or access AWS credentials; Matrix cannot open a path outside its configured workspace.

Priority 1 — establish a trustworthy source boundary

4. Reconcile the dirty worktree before treating it as a machine image

The current checkout has 81 changed/untracked status entries. Some untracked files are required source:

  • .config/helix/languages.toml:1-3 expects jdtls-helix, but .local/bin/jdtls-helix and setup/packages/jdtls are untracked.
  • .config/snooze/jobs-pi:2-4 references a daily-news prompt that is untracked.
  • .pi/agent/extensions/usage.ts:7-9 loads a usage schema at module import, but the only schema is under untracked .local/state/pi/usage.sql.

Other entries are clearly generated: .local/state/, .tmp-pikchr-test.*, pi-session-*.html, dependency trees, caches, and a root package-lock.json without a root package.json.

Action

  • Review intentional modifications in small thematic commits.
  • Track required scripts, prompts, and schemas under source/config paths.
  • Delete the orphan root lockfile and temporary/session artifacts.
  • Add targeted ignores for .local/state/, .tmp-*, and Pi session exports.
  • Keep runtime SQLite files out of the repository and out of the linker’s input.

Avoid broad ignores such as *.html or *.sql; legitimate reports and schemas may be source.

5. Make bootstrap convergent and explicit

The README promises a one-command fresh setup (README.md:5-14). bootstrap.sh reuses any existing checkout without fetching, checking its remote, branch, or dirty state (bootstrap.sh:32-43). It then installs from that possibly stale tree and links; conflicts require a separate manual --force invocation (bootstrap.sh:45-46). Most installers skip an executable found on PATH without checking whether it matches the manifest version (setup/lib/package-installer.sh:60-68, 102-110, 150-160, 178-189, 244-252, 525-529).

Recommendation

  • Fresh clone: retain the current one-command path.
  • Existing checkout: fail clearly if dirty; verify the expected remote; fetch and fast-forward explicitly.
  • Make conflict behavior explicit (fail by default); do not pass destructive force implicitly.
  • Have each versioned installer compare the installed version and converge when the manifest changes.
  • Add a mk doctor target that checks OS, architecture, required commands, secrets, services, and configured jobs without changing the system.

Do not make bootstrap auto-reset or auto-clean a user’s checkout.

6. Fix immediate correctness and filesystem-safety defects

These are small changes with disproportionate value:

  • .local/bin/dmenu_custom_launcher:3 has a malformed ${XDG_CONFIG_HOME...} path and cannot locate its script directory.
  • setup/containers.sh:37-42 runs its loop through a pipeline subshell, allowing clone failures to end with a success message.
  • link.sh:93-97 can follow an existing destination directory symlink and ignores parent-directory creation failure.
  • .local/bin/record.sh:5, 11-23, 74-85 uses predictable /tmp/record.pid; use a private runtime directory, locking, and process identity validation.
  • .local/lib/sh/pick.sh:9-13 uses eval on a selected value; validate a decimal choice and dispatch without eval.
  • .local/bin/gh-pr.sh:4-6 and .local/bin/git-worktree-create.sh:5-7 source tmpfile.sh both directly and through editor.sh, replacing cleanup state and leaking the first temporary root.
  • Stocks/RSS runit scripts ignore cd failure; use cd ... || exit 1 before podman-compose.
  • .config/snooze/reconcile-services.sh:10-13 cleans up on signals but does not exit.
  • .local/bin/p8-worktree-build.sh:66-80 records a worktree as seen before a successful build, preventing retries.

These fixes should be the first ordinary maintenance PR after credential rotation.

Priority 2 — harden automation and network behavior

7. Make scheduled-job output private, bounded, and durable

.local/bin/run-job.py:186-188 creates log directories with inherited permissions and opens logs normally at 226. Observed permissions were 0775 for the directory and 0664 for logs. Jobs can include reminders, finance data, prompts, and command output. The command stream waits indefinitely (230-250), while failure handling may email the full log. SQLite is configured for immediate lock failure and can continue file-only, losing database observability under contention (run-job.py:30-35, 206-221, 243-245).

Recommendation

  • Create the log directory as 0700 and files as 0600.
  • Add a required/default job deadline; terminate then kill the process group.
  • Put timeout and maximum attachment size on failure mail; send a bounded tail by default.
  • Serialize database writers with the existing locking helper, or use a modest busy timeout plus bounded retries.
  • Record degraded logging as a failed or explicitly degraded run rather than silently treating it as normal.

8. Tighten outbound authentication and trust checks

  • .config/snooze/scripts/github-sync.sh:12-14 accepts any remote URL containing the owner text. Match an exact approved host and owner, or use an explicit repository allowlist.
  • .local/lib/sh/mail.sh:54-59 puts username:password in curl arguments. Reuse the Python SMTP implementation or a protected curl config/descriptor.
  • New SSM copy/edit/mount scripts disable host-key checking and discard known hosts. Maintain a dedicated known-hosts file and require explicit first trust.
  • Maven and JDK manager scripts download archives without vendor checksum verification (.local/bin/maven-manager.sh:75-82, .local/bin/java-manager.sh:89-95). Verify published checksums before extraction.
  • .local/bin/publish.py:20-23, 101-108 binds 0.0.0.0 despite being a local publishing helper. Bind loopback by default and require an explicit flag/config for LAN publication.
  • URL-following jobs should require HTTPS redirects and validate final hosts where the source is security-sensitive.

9. Resolve Matrix bridge dependency risk

The tracked Matrix bridge lockfile currently reports eight npm vulnerabilities, including two critical transitive findings through the direct matrix-bot-sdk dependency (.local/src/matrix-pi-bridge/package.json:7; lock entry around package-lock.json:3021). npm audit fix has no automatic resolution.

Action: evaluate a supported SDK version or replacement dependency path. Until resolved, document the risk and compensate by sharply reducing bridge authority. Do not suppress the audit finding.

10. Complete runtime dependency declarations

Configured features rely on commands absent from setup manifests, including rclone, sqlite3, ffmpeg, poppler-utils/pdftotext, fwupdmgr, harper-ls, desktop commands, and the configured browser. .local/bin/md2html.py:6 also imports Python markdown without a declared Python environment.

Choose one of two clear contracts for each feature:

  • Core: declare and install its dependencies.
  • Optional: group it under a named target and have mk doctor say why it is unavailable.

Do not silently install every personal service during basic shell bootstrap. A small core, desktop, and automation split is enough if profiles are needed.

Priority 3 — reproducibility, portability, and maintainability

11. State the platform contract instead of accumulating compatibility branches

The repository says Ubuntu/Debian, but several assets hard-code x86_64 (setup/setup-neovim.sh:5,39, setup/lib/package-installer.sh:467-475, Rust/JDK/archive manifests). Some /bin/sh scripts use implementation-specific commands or extensions such as GNU readlink -f, find -printf, grep -P, sort -V, and sleep 1h.

The simple choice is either:

  • explicitly support Debian/Ubuntu x86_64 with dash + GNU userland, validate it in bootstrap, and stop implying broader portability; or
  • add one architecture mapping helper and test arm64 in CI.

Do not add ad-hoc portability code file by file. First define the contract.

12. Add a minimal, high-value test and validation layer

There is no project-owned test suite or CI workflow. Add only checks that protect the repository’s contracts:

  1. sh -n/bash -n and ShellCheck for declared shell scripts.
  2. Python AST/compile plus Ruff for Python scripts.
  3. TypeScript type-checking for the two Node packages/extensions.
  4. JSON, JSONC, TOML, and YAML parse checks.
  5. A temporary-HOME integration test for link.sh covering tracked-only behavior, conflicts, symlink parents, spaces, idempotence, and stale links.
  6. Manifest validation: duplicate package names, required fields, checksums, supported architecture, and configured command availability.
  7. A history-aware secret scan.
  8. npm audit for Node components, with reviewed exceptions rather than blanket suppression.

A single mk check target should run the same commands locally and in GitHub Actions. Avoid introducing a large test framework; shell fixtures and Python’s standard unittest are sufficient.

13. Repair extension ownership and failure contracts

The current Pi extension manifest now lists agents.ts and usage.ts, but Finance remains orphaned relative to its skill, and the usage extension depends on an untracked runtime schema. Shared readFile handling in .pi/agent/extensions/lib/utils.ts:21-25 converts every read failure into an empty result; Finance can then overwrite a ledger as though it did not exist.

  • Register and update Finance, or delete the orphaned extension and skill.
  • Track schemas under source and create runtime databases explicitly.
  • Treat only ENOENT as “missing”; propagate permission, I/O, and parse errors.
  • Use atomic writes and the existing locking concept for ledgers.

Proposed repository contract

The repository should have four explicit classes of content:

Class Location Git Linked Example
Home configuration paths mirroring $HOME tracked yes .config/helix/config.toml
Setup source setup/, bootstrap.sh, mkfile tracked no package manifests
Documentation/tests README.md, docs/, tests/, .github/ tracked no linker integration test
Runtime/private data XDG state/cache/data, pass, credentials never tracked never from checkout SQLite DB, OAuth tokens, logs

Schemas and examples are source; databases and filled credential files are state. Keep .example files tracked, create private real files outside the repository, and have doctor report missing configuration without printing values.

Phased implementation plan

Phase 0 — today: containment

  • Rotate exposed FreshRSS and Pi credentials.
  • Rewrite reachable Git history and coordinate clone cleanup.
  • Stop default Podman-socket, whole-home, and AWS mounts.
  • Bind the publish service to loopback by default.

Done when: revoked credentials no longer work; history scan passes; default Pi cannot access Podman/AWS/unrelated home files.

Phase 1 — 1–2 focused PRs: restore the boundary

  • Classify every current worktree entry as source, generated state, secret, or discardable artifact.
  • Track required JDTLS/prompt/schema source; ignore or remove generated artifacts.
  • Change link.sh to deploy only git ls-files paths.
  • Add temporary-home linker tests.

Done when: git status --short is empty after normal use, fresh clone reproduces configured tools/jobs, and untracked files cannot become home configuration.

Phase 2 — 1 PR: reliability fixes

  • Fix the launcher typo, container-loop status, symlink-parent handling, temp-library duplication, picker eval, PID file, service cd, signal exit, and build retry state.
  • Make bootstrap reject dirty/stale unexpected checkouts and make versioned installers converge.

Done when: focused regression tests cover each defect and bootstrap reruns safely without replacing user files.

Phase 3 — 1–2 PRs: automation hardening

  • Private logs, process deadlines, bounded mail, SQLite contention handling.
  • Exact GitHub remote allowlist, safe mail authentication, SSM host keys, archive checksums.
  • Declare core/optional dependencies and add mk doctor.

Done when: jobs cannot run forever, private output is 0600, concurrent jobs retain run records, and missing optional dependencies are diagnosed before scheduling.

Phase 4 — continuous: validation and documentation

  • Add mk check and a small GitHub Actions workflow.
  • Document platform/architecture contract, package-manifest syntax, update/checksum procedure, profiles, rollback, services, secrets, and recovery.
  • Link docs/reminders.md and docs/scheduled-jobs.md from the README.
  • Resolve or replace the vulnerable Matrix SDK path.

Done when: a pull request cannot merge with broken syntax, invalid config, leaked secrets, unsafe linker behavior, or unexplained critical dependency findings.

Suggested first five commits

  1. security: rotate credentials and purge leaked history — operational/history change, isolated from code.
  2. link: deploy only Git-tracked home files — linker plus temporary-home tests.
  3. repo: separate source from generated state — schema/prompt/JDTLS decisions and targeted ignores.
  4. scripts: fix launcher, temp, PID, service, and failure propagation — small correctness fixes.
  5. check: add mk check and CI — syntax, config, linker, manifest, and secret checks.

Keep these commits separate so each security and behavioral decision is auditable and reversible.

What not to do

Audit notes and evidence limits

The audit examined the current dirty worktree, tracked history references, setup/linking code, shell/Python/TypeScript automation, service definitions, and configuration files. It ran non-destructive syntax and parse checks; no full bootstrap was executed because that would install packages and modify the host. No credential values are reproduced in this report.

One automated audit subprocess reported that an erroneous cleanup probe attempted to remove its $HOME and timed out on permission errors. The repository remained present, but filesystem effects outside the repository were not exhaustively verified. Review recent home-directory changes/backups before relying on this audit as proof that unrelated local files were untouched.