Skip to content

fix(config): fix directory locations to follow XDG spec#336

Merged
jdx merged 8 commits intomainfrom
refactor/lease-state-dir
Mar 8, 2026
Merged

fix(config): fix directory locations to follow XDG spec#336
jdx merged 8 commits intomainfrom
refactor/lease-state-dir

Conversation

@jdx
Copy link
Copy Markdown
Owner

@jdx jdx commented Mar 8, 2026

Summary

  • Moves lease ledger storage from ~/.config/fnox/leases/ to ~/.local/state/fnox/leases/ (XDG state dir)
  • Adds FNOX_STATE_DIR env var for overriding the state directory
  • Automatically migrates existing ledger files (and lock files) on first access

Test plan

  • Verify existing leases at ~/.config/fnox/leases/ are migrated on first fnox exec or fnox lease list
  • Verify new leases are created under ~/.local/state/fnox/leases/
  • Verify FNOX_STATE_DIR override works
  • Verify fallback to old path if migration fails (e.g., permission denied)

🤖 Generated with Claude Code


Note

Medium Risk
Changes on-disk locations for the lease ledger and config/state resolution, which can break existing caches or lookups in environments relying on prior paths or non-absolute overrides.

Overview
Updates fnox’s directory resolution to follow the XDG spec: FNOX_CONFIG_DIR now honors XDG_CONFIG_HOME, and a new FNOX_STATE_DIR (with XDG_STATE_HOME fallback) is introduced for stateful data. Environment path parsing (var_path) now ignores empty values and rejects non-absolute paths.

Moves the per-project lease ledger from the config directory to the XDG state directory (~/.local/state/fnox/leases/...) and updates the leases guide accordingly. Test harnesses are adjusted to avoid CI/user XDG env leaking into tests, and Bitwarden bats tests preserve BITWARDENCLI_APPDATA_DIR before HOME is rewritten.

Written by Cursor Bugbot for commit b6a93ee. This will update automatically on new commits. Configure here.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request refactors the storage location for fnox lease ledgers to adhere to the XDG Base Directory Specification, moving them from the configuration directory to the state directory. This change improves file system organization by separating configuration from application state. It also provides users with more control over the state directory via an environment variable and ensures a smooth transition for existing users by implementing automatic migration of their current lease data.

Highlights

  • Lease Ledger Location Change: The lease ledger storage has been moved from ~/.config/fnox/leases/ to ~/.local/state/fnox/leases/, aligning with the XDG Base Directory Specification for state files.
  • Environment Variable for State Directory: A new environment variable, FNOX_STATE_DIR, has been introduced, allowing users to override the default state directory path for fnox.
  • Automatic Migration: Existing lease ledger files and their associated lock files will now be automatically migrated from the old configuration directory to the new state directory upon first access.
Changelog
  • docs/guide/leases.md
    • Updated the documented path for lease credentials storage to reflect the new XDG state directory location.
  • src/env.rs
    • Introduced FNOX_STATE_DIR as a new static variable to define the application's state directory.
    • Configured FNOX_STATE_DIR to be overrideable via the FNOX_STATE_DIR environment variable.
    • Set platform-specific default paths for FNOX_STATE_DIR: ~/.local/state/fnox for Unix and ~/AppData/Local/fnox for Windows.
  • src/lease.rs
    • Modified the LeaseLedger::ledger_path function to construct paths using the newly defined FNOX_STATE_DIR.
    • Implemented migration logic to check for existing lease ledger files in the old ~/.config/fnox/leases/ location.
    • Added automatic renaming of old ledger files and their corresponding lock files to the new state directory.
    • Included error handling and logging for migration failures, with a fallback to the old path if migration is unsuccessful.
Activity
  • The pull request description indicates that it was generated with Claude Code.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the storage location for the lease ledger to follow XDG standards, moving it from the configuration directory to a state directory. This architectural improvement includes adding a new FNOX_STATE_DIR environment variable and implementing automatic migration for existing lease files. However, the migration logic introduces a high-severity race condition that can lead to data loss if multiple processes access the ledger concurrently during the first run after an upgrade. A process failing migration due to a concurrent move could fall back to an empty ledger and overwrite successfully migrated data. It is recommended to perform the migration under a stable lock or ensure consistent path resolution. Furthermore, the error handling for directory creation and lock file migration in src/lease.rs needs improvement to prevent potential data loss or race conditions, and the new state directory path on Windows in src/env.rs is currently identical to the configuration directory, which undermines the separation goal.

I am having trouble creating individual review comments. Click here to see my feedback.

src/lease.rs (89-114)

security-high high

The ledger_path migration logic is highly susceptible to a race condition, which can lead to data loss. When multiple processes attempt to access the lease ledger for the first time after an upgrade, a non-atomic check-and-rename sequence without proper file locking can result in one process overwriting successfully migrated data with an empty ledger if it fails a concurrent rename and falls back to the old path.

Furthermore, the current implementation has several robustness issues:

  • Error Handling for create_dir_all: Errors during directory creation for new_path are silently ignored. This can lead to subsequent file operation failures, and the migration should fall back to old_path in such cases.
  • Error Handling for Lock File Migration: The result of renaming the lock file (fs::rename(&old_lock, &new_lock)) is ignored. If the ledger file migrates but the lock file doesn't, it could introduce further race conditions or inconsistent locking behavior. This failure should at least be logged.
  • Readability: The deeply nested logic makes it harder to follow and debug. Refactoring to handle simple cases first would improve clarity and maintainability.
    fn ledger_path(project_dir: &Path) -> PathBuf {
        let hash = hash_project_dir(project_dir);
        let filename = format!("{hash}.toml");
        let new_path = env::FNOX_STATE_DIR.join("leases").join(&filename);
        let old_path = env::FNOX_CONFIG_DIR.join("leases").join(&filename);

        // If new path exists, or old path doesn't, no migration is needed.
        if new_path.exists() || !old_path.exists() {
            return new_path;
        }

        // New path does not exist, but old one does. Attempt migration.
        tracing::debug!(
            "Lease ledger at old path {} found, attempting migration to {}",
            old_path.display(),
            new_path.display()
        );

        if let Some(parent) = new_path.parent() {
            if let Err(e) = fs::create_dir_all(parent) {
                tracing::warn!(
                    "Failed to create directory for lease ledger migration {}: {}. Falling back to old path.",
                    parent.display(),
                    e
                );
                return old_path;
            }
        } else {
            // This is unlikely, but worth handling.
            tracing::warn!(
                "Could not determine parent directory for {}. Falling back to old path.",
                new_path.display()
            );
            return old_path;
        }

        // Migrate the ledger file.
        if let Err(e) = fs::rename(&old_path, &new_path) {
            tracing::warn!(
                "Failed to migrate lease ledger from {} to {}: {}. Falling back to old path.",
                old_path.display(),
                new_path.display(),
                e
            );
            return old_path;
        }

        tracing::debug!("Migrated lease ledger to {}", new_path.display());

        // Also migrate the lock file if present.
        let old_lock = old_path.with_extension("lock");
        if old_lock.exists() {
            let new_lock = new_path.with_extension("lock");
            if let Err(e) = fs::rename(&old_lock, &new_lock) {
                tracing::warn!(
                    "Failed to migrate lease lock file from {} to {}: {}. This may cause locking issues.",
                    old_lock.display(),
                    new_lock.display(),
                    e
                );
            }
        }

        new_path
    }

src/env.rs (36-37)

medium

On Windows, both FNOX_STATE_DIR and the existing FNOX_CONFIG_DIR are being set to the same path (~/AppData/Local/fnox). This defeats the purpose of separating state from configuration on the Windows platform. As a result, the migration logic in src/lease.rs will not trigger for Windows users as old_path and new_path will be identical.

To properly align with the goal of this refactor, the state directory should be distinct from the configuration directory on Windows as well. A common pattern is to use sibling directories or subdirectories within AppData/Local.

Since changing FNOX_CONFIG_DIR would be a breaking change for existing Windows users, I suggest modifying the default path for FNOX_STATE_DIR on Windows to ensure it's unique.

        #[cfg(windows)]
        let default = HOME_DIR.join("AppData").join("Local").join("fnox-state");

Comment thread src/env.rs
Comment thread src/lease.rs
fn ledger_path(project_dir: &Path) -> PathBuf {
let hash = hash_project_dir(project_dir);
env::FNOX_CONFIG_DIR
env::FNOX_STATE_DIR
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No migration for existing lease ledger files

Medium Severity

The ledger_path now points to FNOX_STATE_DIR instead of FNOX_CONFIG_DIR, but the load method returns an empty default ledger when the new path doesn't exist. Existing ledger files at ~/.config/fnox/leases/ are silently orphaned — no migration code exists despite the PR summary claiming "Automatically migrates existing ledger files (and lock files) on first access." This causes existing cached leases to be lost and unnecessarily re-created.

Fix in Cursor Fix in Web

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Mar 8, 2026

Greptile Summary

This PR moves lease ledger storage from ~/.config/fnox/leases/ to ~/.local/state/fnox/leases/ to comply with the XDG Base Directory Specification, adds a new FNOX_STATE_DIR env var, tightens path env var validation, and fixes test isolation for XDG variables.

Key changes:

  • src/env.rs: Adds FNOX_STATE_DIR static backed by XDG_STATE_HOME, and var_path now rejects empty strings and relative paths per XDG spec.
  • src/lease.rs: LeaseLedger::ledger_path switches from FNOX_CONFIG_DIR to FNOX_STATE_DIR.
  • test/bitwarden.bats: Captures BITWARDENCLI_APPDATA_DIR before _common_setup redirects HOME, fixing Bitwarden CLI auth in tests.
  • test/test_helper/common_setup.bash: Unsets XDG_CONFIG_HOME and XDG_STATE_HOME before each test to prevent ambient CI vars from interfering.

Issues found:

  • Missing migration code (critical): The PR description and test plan both state that existing ledger files at ~/.config/fnox/leases/ will be automatically migrated on first access — but no such migration logic exists anywhere in the implementation. LeaseLedger::load simply returns an empty default if the new path doesn't exist. Existing users will silently lose all cached credentials on upgrade.
  • Silent rejection of invalid env var paths: When FNOX_STATE_DIR or FNOX_CONFIG_DIR is set to a relative or empty value, the fallback to the XDG default happens silently. This is inconsistent with FNOX_PROFILE, which emits a Warning: message when its value is invalid.

Confidence Score: 2/5

  • Not safe to merge — the promised automatic migration of existing lease ledger files is absent, which will cause silent data loss for all existing users on upgrade.
  • The core directory-layout change and env var validation are correct, but the PR description explicitly promises automatic migration of ~/.config/fnox/leases/~/.local/state/fnox/leases/ that is completely unimplemented. Without it, every existing user silently loses their cached credentials and will be forced to re-authenticate. The Windows path collision and macOS path-change issues flagged in prior threads remain open as well.
  • src/lease.rs — migration logic must be added to LeaseLedger::load before this can safely ship to existing users.

Important Files Changed

Filename Overview
src/lease.rs Switches ledger path from FNOX_CONFIG_DIR to FNOX_STATE_DIR — the change itself is a one-liner and correct, but the promised automatic migration of existing ledger files from the old location is completely absent from the implementation.
src/env.rs Adds FNOX_STATE_DIR static with XDG_STATE_HOME fallback and tightens var_path to reject empty/relative values; Windows collision and macOS path change were flagged in prior threads. New silent rejection of invalid paths provides no user-facing feedback unlike the existing FNOX_PROFILE warning.
test/bitwarden.bats Correctly captures BITWARDENCLI_APPDATA_DIR before _common_setup redirects HOME and unsets XDG_CONFIG_HOME, preserving the real Bitwarden CLI config location for integration tests.
test/test_helper/common_setup.bash Adds unset of XDG_CONFIG_HOME and XDG_STATE_HOME so tests that redirect HOME to a temp directory aren't broken by ambient CI XDG vars — a clean, correct fix.
docs/guide/leases.md Documentation updated to reflect the new ~/.local/state/fnox/leases/ path; straightforward and accurate.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[LeaseLedger::load / lock / save] --> B{FNOX_STATE_DIR set\nand absolute?}
    B -- yes --> C[Use FNOX_STATE_DIR/leases/hash.toml]
    B -- no --> D{XDG_STATE_HOME set\nand absolute?}
    D -- yes --> E[Use XDG_STATE_HOME/fnox/leases/hash.toml]
    D -- no --> F{Platform}
    F -- Unix --> G[~/.local/state/fnox/leases/hash.toml]
    F -- Windows --> H[~/AppData/Local/fnox/leases/hash.toml]

    C --> I[Check path.exists?]
    E --> I
    G --> I
    H --> I

    I -- exists --> J[Read & deserialize TOML]
    I -- missing --> K[⚠️ Return empty default\nNO migration attempted\nOld ~/.config/fnox/leases/ silently ignored]

    style K fill:#f96,color:#000
Loading

Comments Outside Diff (2)

  1. src/lease.rs, line 108-119 (link)

    Migration code promised but not implemented

    The PR description explicitly states "Automatically migrates existing ledger files (and lock files) on first access," and the test plan includes a step to verify migration of ~/.config/fnox/leases/. However, LeaseLedger::load only checks the new FNOX_STATE_DIR-based path. If the file doesn't exist there, it silently returns an empty ledger:

    if !path.exists() {
        return Ok(Self::default());
    }

    There is no code that looks for ledger files at the old FNOX_CONFIG_DIR.join("leases") path or copies/moves them to the new location. For any existing user with leases cached at ~/.config/fnox/leases/<hash>.toml, all cached credentials will be silently dropped on upgrade — fnox will just create new leases instead of reusing the cached ones.

    The advertised migration should be implemented before merging, something like:

    pub fn load(project_dir: &Path) -> Result<Self> {
        let path = Self::ledger_path(project_dir);
        if !path.exists() {
            // Attempt migration from old config-dir location
            let old_path = env::FNOX_CONFIG_DIR
                .join("leases")
                .join(path.file_name().unwrap());
            if old_path.exists() {
                if let Some(parent) = path.parent() {
                    let _ = fs::create_dir_all(parent);
                }
                let _ = fs::rename(&old_path, &path)
                    .or_else(|_| fs::copy(&old_path, &path).map(|_| ()));
                let _ = fs::remove_file(old_path.with_extension("lock"));
            }
            if !path.exists() {
                return Ok(Self::default());
            }
        }
        // … rest of load
    }
  2. src/env.rs, line 71-77 (link)

    Silent rejection of invalid env var paths produces no user feedback

    When FNOX_STATE_DIR (or FNOX_CONFIG_DIR) is set to a relative path or an empty string, var_path discards it silently and falls back to the XDG default. This is inconsistent with FNOX_PROFILE, which explicitly prints a warning:

    // From FNOX_PROFILE handling:
    eprintln!("Warning: Invalid FNOX_PROFILE value '{}' ignored …", profile);

    A user who sets FNOX_STATE_DIR=my/relative/path will see their state land in an unexpected default location with no indication that their configuration was ignored. Consider emitting a warning analogous to the profile-name case:

    fn var_path(name: &str) -> Option<PathBuf> {
        let raw = var(name).ok()?;
        if raw.is_empty() {
            return None;
        }
        let p = PathBuf::from(&raw);
        if !p.is_absolute() {
            eprintln!("Warning: {name} value '{raw}' is not an absolute path and will be ignored");
            return None;
        }
        Some(p)
    }

Fix All in Claude Code

Last reviewed commit: b6a93ee

Comment thread src/env.rs
@jdx jdx changed the title refactor(config): move lease ledger to ~/.local/state/fnox fix(config): fix directory locations to follow XDG spec Mar 8, 2026
Comment thread src/env.rs
@jdx jdx enabled auto-merge (squash) March 8, 2026 21:57
@jdx jdx force-pushed the refactor/lease-state-dir branch from 70170f8 to f9b23f1 Compare March 8, 2026 22:17
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment thread src/env.rs
jdx and others added 8 commits March 8, 2026 19:26
Lease ledgers are runtime state, not configuration, so they belong
under XDG_STATE_HOME (~/.local/state) rather than XDG_CONFIG_HOME
(~/.config). Adds FNOX_STATE_DIR env var and automatic migration
from the old location.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use dirs::config_dir() and dirs::state_dir() instead of hardcoding
~/.config and ~/.local/state paths, so XDG environment variables
are respected.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…paths

dirs::config_dir() returns ~/Library/Application Support on macOS,
breaking the expected ~/.config/fnox path. Use $XDG_CONFIG_HOME and
$XDG_STATE_HOME directly with ~/.config and ~/.local/state fallbacks,
which is consistent across all platforms.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…dirs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The XDG Base Directory spec requires that empty values be treated as
unset and all paths be absolute. An empty XDG_STATE_HOME would produce
a relative path causing ledger files to be written to the cwd.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests set HOME to a temp dir and create configs at $HOME/.config/fnox/.
If XDG_CONFIG_HOME is set (e.g. in CI), it takes precedence and fnox
looks in the wrong directory, causing "No providers configured" errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The bw CLI stores server URL and login state in
~/.config/Bitwarden CLI/data.json. When _common_setup changes HOME
to a temp dir, bw can't find its config and reports "You are not
logged in" even with BW_SESSION set. Fix by capturing
BITWARDENCLI_APPDATA_DIR before HOME changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jdx jdx force-pushed the refactor/lease-state-dir branch from 7d3dae8 to b6a93ee Compare March 8, 2026 23:26
@jdx jdx merged commit 91828a1 into main Mar 8, 2026
15 checks passed
@jdx jdx deleted the refactor/lease-state-dir branch March 8, 2026 23:36
jdx pushed a commit that referenced this pull request Mar 9, 2026
### 🚀 Features

- **(cloudflare)** add Cloudflare API token lease backend by
[@jdx](https://github.com/jdx) in
[#335](#335)
- **(fido2)** bump demand to v2, mask PIN during typing by
[@jdx](https://github.com/jdx) in
[#334](#334)
- **(init)** add -f as short alias for --force by
[@jdx](https://github.com/jdx) in
[#329](#329)
- **(lease)** add --all flag, default to creating all leases by
[@jdx](https://github.com/jdx) in
[#337](#337)
- **(lease)** add GitHub App installation token lease backend by
[@jdx](https://github.com/jdx) in
[#342](#342)

### 🐛 Bug Fixes

- **(config)** fix directory locations to follow XDG spec by
[@jdx](https://github.com/jdx) in
[#336](#336)
- **(exec)** use unix exec and exit silently on subprocess failure by
[@jdx](https://github.com/jdx) in
[#339](#339)
- **(fido2)** remove duplicate touch prompt by
[@jdx](https://github.com/jdx) in
[#332](#332)
- **(set)** write to lowest-priority existing config file by
[@jdx](https://github.com/jdx) in
[#331](#331)
- **(tui)** skip providers requiring interactive auth by
[@jdx](https://github.com/jdx) in
[#333](#333)

### 🛡️ Security

- **(ci)** retry lint step to handle transient pkl fetch failures by
[@jdx](https://github.com/jdx) in
[#341](#341)
- **(mcp)** add MCP server for secret-gated AI agent access by
[@jdx](https://github.com/jdx) in
[#343](#343)
- add guide for fnox sync by [@jdx](https://github.com/jdx) in
[#328](#328)

### 🔍 Other Changes

- share Rust cache across CI jobs by [@jdx](https://github.com/jdx) in
[#340](#340)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant