{"record":{"id":"985222843a9ef763","repo":"gitbutlerapp/gitbutler","slug":"configured-storage-path-resolves-inside-git","errorCode":null,"errorMessage":"configured storage path '{}' resolves inside '.git' but not under a top-level 'gitbutler*' directory","messagePattern":"configured storage path '(.+?)' resolves inside '\\.git' but not under a top-level 'gitbutler\\*' directory","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/but-project-handle/src/storage_path.rs","lineNumber":102,"sourceCode":"fn validate_in_git_storage_path(\n    storage_path_relative_to_git_dir: &Path,\n    storage_path: &Path,\n) -> anyhow::Result<()> {\n    let Some(Component::Normal(top_level_dir)) =\n        storage_path_relative_to_git_dir.components().next()\n    else {\n        bail!(\n            \"configured storage path '{}' resolves to '.git' itself; choose a dedicated GitButler directory instead\",\n            storage_path.display()\n        );\n    };\n\n    if !top_level_dir\n        .to_string_lossy()\n        .get(..DEFAULT_STORAGE_DIR_NAME.len())\n        .is_some_and(|name| name.eq_ignore_ascii_case(DEFAULT_STORAGE_DIR_NAME))\n    {\n        bail!(\n            \"configured storage path '{}' resolves inside '.git' but not under a top-level 'gitbutler*' directory\",\n            storage_path.display()\n        );\n    }\n\n    Ok(())\n}\n\n/// Name of the default GitButler storage directory inside the git dir.\npub const DEFAULT_STORAGE_DIR_NAME: &str = \"gitbutler\";\n\n/// Git-dir-relative path of the refresh sentinel, `gitbutler/REFRESH`.\n///\n/// Single source of truth shared by the writer (`write_refresh_sentinel`) and\n/// the watcher (`gitbutler_filemonitor`).\npub const REFRESH_SENTINEL_PATH: &str = \"gitbutler/REFRESH\";\n\n/// Identity written into the refresh sentinel so a process can skip the write","sourceCodeStart":84,"sourceCodeEnd":120,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-project-handle/src/storage_path.rs#L84-L120","documentation":"The second in-git storage-path rule: the path must sit under a top-level `.git/<dir>` whose name starts with 'gitbutler' (case-insensitive). Anything else inside .git — 'info', 'objects', 'hooks', or arbitrary custom directories — could collide with git's own files and is rejected.","triggerScenarios":"Configuring `storage_path_relative_to_git_dir` to a directory whose first component is not 'gitbutler*', e.g. 'info/gitbutler', 'my-storage', or a nested path under a non-gitbutler folder.","commonSituations":"Attempts to hide GitButler data inside git's own directories; typos like 'gitbutle' or 'git-butler'; copying configs between machines with different conventions.","solutions":["Use a top-level directory starting with 'gitbutler', e.g. 'gitbutler' or 'gitbutler-work'","Point the storage path outside .git via the non-in-git override if a different location is really needed","Remove the override to use the default .git/gitbutler"],"exampleFix":"# before\nstorage_path_relative_to_git_dir = \"info/gitbutler\"\n\n# after\nstorage_path_relative_to_git_dir = \"gitbutler-work\"","handlingStrategy":"validation","validationCode":"// Rust: require a top-level 'gitbutler*' directory\nfn under_gitbutler_dir(rel: &std::path::Path) -> bool {\n    matches!(rel.components().next(), Some(std::path::Component::Normal(c))\n        if c.to_string_lossy().get(..9).is_some_and(|n| n.eq_ignore_ascii_case(\"gitbutler\")))\n}\nassert!(under_gitbutler_dir(configured_rel), \"storage must live under .git/gitbutler*\");","typeGuard":null,"tryCatchPattern":"Catch the bail from settings save and prompt with the two valid choices: a top-level 'gitbutler*' directory under .git, or a location outside .git.","preventionTips":["Offer a preset list of valid storage paths in the UI instead of free-form input","Beware case-insensitive filesystems: 'GitButler' matches, 'git-butler' does not"],"tags":["config","storage-path","git-dir","validation","gitbutler"],"backgroundTag":"invalid-config-path","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}