From 8f0e6a41a8ec93b1f93e7e22054c3f0af491f944 Mon Sep 17 00:00:00 2001 From: James Maes Date: Sat, 10 Oct 2026 13:47:08 -0500 Subject: [PATCH] fix: keep per-user data out of the Velopack install root Installing a Setup.exe over an existing install deleted accounts.json, settings.json and the logs: Velopack uninstalls the previous version first and removes the whole install root, and gclo kept its data in that same folder (%LOCALAPPDATA%\gclo). The default data root is now %LOCALAPPDATA%\KofTwentyTwo\gclo for the app, the CLI and the activity log; GCLO_DATA_DIR still overrides it. No migration: the app has a single user on a single machine today. The default vault scope stays unscoped, so existing Credential Manager entries keep working. Docs, threat model and tests follow. Closes #91 Co-Authored-By: Claude Fable 5.1 Signed-off-by: James Maes --- README.md | 2 +- docs/security/threat-model.md | 3 +- gclo.Engine.Tests/ActivityLogTests.cs | 17 +++++++++-- gclo.Engine.Tests/GcloPathsTests.cs | 44 +++++++++++++++++++++++++++ gclo.Engine/FileActivityLog.cs | 6 ++-- gclo.ViewModels/AccountsStore.cs | 2 +- gclo.ViewModels/AppSettings.cs | 2 +- gclo.ViewModels/GcloPaths.cs | 27 +++++++++++----- 8 files changed, 87 insertions(+), 16 deletions(-) create mode 100644 gclo.Engine.Tests/GcloPathsTests.cs diff --git a/README.md b/README.md index 17ccefb..33574dc 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ On launch, gclo shows a brief branded splash, then the **Quick Sync** connect ca **Settings** (File > Settings…) let you set a default GitHub token (stored in the Windows Credential Manager), the default target folder (with a folder picker), the default parallelism, and the theme (System, Light, or Dark); an Advanced section toggles the startup splash and its duration. -**Activity log:** View > Activity log… shows the tail of today's log and links to the logs folder. Logs live under `%LOCALAPPDATA%\gclo\logs`, one file per day; they record run parameters and per-repo failures and never contain your token (see [SECURITY.md](SECURITY.md)). +**Activity log:** View > Activity log… shows the tail of today's log and links to the logs folder. Logs live under `%LOCALAPPDATA%\KofTwentyTwo\gclo\logs`, one file per day; they record run parameters and per-repo failures and never contain your token (see [SECURITY.md](SECURITY.md)). **Updates:** Help > Check for updates… downloads and applies the latest release for your channel. Self-update is only available in installed builds (not the portable zip or a local debug build). diff --git a/docs/security/threat-model.md b/docs/security/threat-model.md index 3ead58b..587265b 100644 --- a/docs/security/threat-model.md +++ b/docs/security/threat-model.md @@ -26,7 +26,7 @@ GitHub. | GitHub REST API | Lists organizations and repositories | Untrusted input (responses are parsed), authenticated by the PAT | | Git remotes (`github.com`) | Clone and fetch over HTTPS via libgit2 | Untrusted input (repository content, including file paths) | | Windows Credential Manager | Stores saved-account and default tokens | Trusted store; boundary is the Windows user account | -| Local data directory | `%LOCALAPPDATA%\gclo` (or `GCLO_DATA_DIR`): settings, accounts, logs | Trusted; same user | +| Local data directory | `%LOCALAPPDATA%\KofTwentyTwo\gclo` (or `GCLO_DATA_DIR`): settings, accounts, logs. Deliberately outside the Velopack install root `%LOCALAPPDATA%\gclo`, which Setup.exe deletes on reinstall (#91) | Trusted; same user | | Target folder | Where repositories are written; may already contain repositories | Partly untrusted (pre-existing content, `.git` config) | | WSL default distribution | Runs `git clone` for the "Clone in WSL" recovery | Semi-trusted (user-installed Linux environment) | | Update feed | GitHub Releases of this repository, `releases..json` | Untrusted until verified (HTTPS, package hashes, attestations) | @@ -112,3 +112,4 @@ flowchart LR | 2026-10-09 | v1.0.1 | James Maes (@KofTwentyTwo) | Initial model, covering the path-recovery, WSL clone, vault scoping, package-manager, and SBOM changes of 1.0.1 | | 2026-10-09 | v1.0.1 | James Maes (@KofTwentyTwo) | T11 updated: releases build in the shared standards workflow; publishing tokens moved to the `release` environment (#76) | | 2026-10-10 | v1.0.2 | James Maes (@KofTwentyTwo) | T11: the packaging PR is opened with a short-lived release GitHub App token (contents + pull requests on this repository, no bypass) instead of a long-lived PAT; the commit is GitHub-signed (#85) | +| 2026-10-10 | v1.0.2 | James Maes (@KofTwentyTwo) | Local data directory moved to `%LOCALAPPDATA%\KofTwentyTwo\gclo`: the Velopack install root is deleted by Setup.exe, which destroyed accounts.json and settings.json (#91) | diff --git a/gclo.Engine.Tests/ActivityLogTests.cs b/gclo.Engine.Tests/ActivityLogTests.cs index 99c1f70..7d353c1 100644 --- a/gclo.Engine.Tests/ActivityLogTests.cs +++ b/gclo.Engine.Tests/ActivityLogTests.cs @@ -128,18 +128,31 @@ public sealed class ActivityLogTests : IDisposable [Fact] - public void DefaultDirectory_IsLocalAppDataGcloLogs() + public void DefaultDirectory_IsLocalAppDataKofTwentyTwoGcloLogs() { var log = new FileActivityLog(); string expected = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "gclo", "logs"); + "KofTwentyTwo", "gclo", "logs"); Assert.Equal(expected, log.LogDirectory); } + [Fact] + public void DefaultDirectory_IsNotInsideTheVelopackInstallRoot() + { + // Setup.exe deletes %LOCALAPPDATA%\gclo (the install root) on reinstall (#91). + string installRoot = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "gclo"); + + Assert.False( + new FileActivityLog().LogDirectory.StartsWith(installRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)); + } + + + [Fact] public void Directory_IsCreatedLazily_OnFirstWriteNotInConstructor() { diff --git a/gclo.Engine.Tests/GcloPathsTests.cs b/gclo.Engine.Tests/GcloPathsTests.cs new file mode 100644 index 0000000..3c84e13 --- /dev/null +++ b/gclo.Engine.Tests/GcloPathsTests.cs @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2026 James Maes (KofTwentyTwo) + * SPDX-License-Identifier: MIT + */ + +using gclo.ViewModels; + + +namespace gclo.Engine.Tests; + + +/// +/// Where gclo keeps its per-user data. The default must stay outside the Velopack +/// install root (%LOCALAPPDATA%\gclo): Setup.exe uninstalls an existing install +/// first and deletes that folder, which destroyed accounts.json and settings.json +/// in 1.0.2-beta.1 (#91). +/// +public sealed class GcloPathsTests +{ + /// The default root is %LOCALAPPDATA%\KofTwentyTwo\gclo. + [Fact] + public void DefaultDataRoot_IsLocalAppDataKofTwentyTwoGclo() + { + string expected = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "KofTwentyTwo", "gclo"); + + Assert.Equal(expected, GcloPaths.DefaultDataRoot); + } + + + + /// The default root is never the install root nor anything under it. + [Fact] + public void DefaultDataRoot_IsNotTheVelopackInstallRoot() + { + string installRoot = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "gclo"); + + Assert.NotEqual(installRoot, GcloPaths.DefaultDataRoot, StringComparer.OrdinalIgnoreCase); + Assert.False( + GcloPaths.DefaultDataRoot.StartsWith(installRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)); + } +} diff --git a/gclo.Engine/FileActivityLog.cs b/gclo.Engine/FileActivityLog.cs index d263c79..c425732 100644 --- a/gclo.Engine/FileActivityLog.cs +++ b/gclo.Engine/FileActivityLog.cs @@ -25,14 +25,16 @@ public sealed class FileActivityLog : IActivityLog /// Creates a log that writes under . /// - /// Folder for the log files; defaults to %LOCALAPPDATA%\gclo\logs when null or blank. + /// Folder for the log files; defaults to %LOCALAPPDATA%\KofTwentyTwo\gclo\logs + /// when null or blank (the same data root as the app; never the Velopack + /// install root %LOCALAPPDATA%\gclo, which Setup.exe deletes, #91). /// public FileActivityLog(string? directory = null) { LogDirectory = string.IsNullOrWhiteSpace(directory) ? Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "gclo", "logs") + "KofTwentyTwo", "gclo", "logs") : directory; } diff --git a/gclo.ViewModels/AccountsStore.cs b/gclo.ViewModels/AccountsStore.cs index f47f5b9..69969ee 100644 --- a/gclo.ViewModels/AccountsStore.cs +++ b/gclo.ViewModels/AccountsStore.cs @@ -12,7 +12,7 @@ namespace gclo.ViewModels; /// /// Persists account profiles as JSON at accounts.json under -/// (default %LOCALAPPDATA%\gclo) and their +/// (default %LOCALAPPDATA%\KofTwentyTwo\gclo) and their /// tokens in an ; the file holds metadata only and never a /// token. Unlike , accounts are primary user data, so write /// failures in , , and diff --git a/gclo.ViewModels/AppSettings.cs b/gclo.ViewModels/AppSettings.cs index 4db55ce..5c96ee4 100644 --- a/gclo.ViewModels/AppSettings.cs +++ b/gclo.ViewModels/AppSettings.cs @@ -11,7 +11,7 @@ namespace gclo.ViewModels; /// /// User preferences persisted as JSON at settings.json under -/// (default %LOCALAPPDATA%\gclo, overridable via +/// (default %LOCALAPPDATA%\KofTwentyTwo\gclo, overridable via /// GCLO_DATA_DIR). Uses Environment.GetFolderPath (not Windows.Storage.ApplicationData) /// so the same path works whether the app runs packaged or unpackaged. /// diff --git a/gclo.ViewModels/GcloPaths.cs b/gclo.ViewModels/GcloPaths.cs index a798e9a..62467d0 100644 --- a/gclo.ViewModels/GcloPaths.cs +++ b/gclo.ViewModels/GcloPaths.cs @@ -8,17 +8,23 @@ namespace gclo.ViewModels; /// /// The single seam for where gclo keeps its per-user data (settings, accounts, -/// logs). Defaults to %LOCALAPPDATA%\gclo; the GCLO_DATA_DIR environment variable -/// overrides it so UI tests (and portable setups) can point the whole app at an -/// isolated directory without touching the real profile. Components that accept -/// an explicit directory (test seams) are unaffected — this only feeds defaults. +/// logs). Defaults to %LOCALAPPDATA%\KofTwentyTwo\gclo; the GCLO_DATA_DIR +/// environment variable overrides it so UI tests (and portable setups) can point +/// the whole app at an isolated directory without touching the real profile. +/// Components that accept an explicit directory (test seams) are unaffected — +/// this only feeds defaults. /// +/// +/// The data root must never be the Velopack install root (%LOCALAPPDATA%\gclo): +/// Setup.exe uninstalls an existing install first and deletes that whole folder, +/// which destroyed the accounts and settings stored there (#91). +/// public static class GcloPaths { /// /// Root directory for gclo's per-user data: GCLO_DATA_DIR when set and - /// non-empty, otherwise %LOCALAPPDATA%\gclo. Read on every access so a - /// value set before launch always wins over any cached default. + /// non-empty, otherwise . Read on every access so + /// a value set before launch always wins over any cached default. /// public static string DataRoot => Environment.GetEnvironmentVariable("GCLO_DATA_DIR") is { Length: > 0 } dir @@ -27,7 +33,12 @@ public static class GcloPaths - /// %LOCALAPPDATA%\gclo: the root used when GCLO_DATA_DIR is not set. + /// + /// %LOCALAPPDATA%\KofTwentyTwo\gclo: the root used when GCLO_DATA_DIR is not + /// set. Outside the Velopack install root on purpose (see the type remarks). + /// public static string DefaultDataRoot => - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "gclo"); + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "KofTwentyTwo", "gclo"); } -- 2.54.0