os_paths
OS-specific standard directories for Flutter and Dart — XDG on Linux/BSD,
~/Library on macOS, %APPDATA% on Windows.
Pure Dart: no plugin registration, no platform channels, no dependencies. The
same code works in a Flutter desktop app, a Flutter test, and a dart:io
command-line program.
Usage
import 'package:os_paths/os_paths.dart';
final paths = OsPaths.instance;
paths.configHome; // ~/.config | ~/Library/Application Support | %APPDATA%
paths.dataHome; // ~/.local/share | ~/Library/Application Support | %APPDATA%
paths.cacheHome; // ~/.cache | ~/Library/Caches | %LOCALAPPDATA%
paths.stateHome; // ~/.local/state | ~/Library/Application Support | %LOCALAPPDATA%
paths.runtimeDir; // $XDG_RUNTIME_DIR | null | null
paths.documentsDir; // ~/Documents (localized via xdg-user-dirs)
Most applications want directories scoped to themselves:
import 'dart:io';
final paths = OsPaths.instance;
final app = paths.app('Fern Notes', organization: 'Lexmata');
final configDir = await app.ensureConfig(); // created if missing
final settings = File(paths.join(configDir.path, 'settings.json'));
await settings.writeAsString('{"theme":"dark"}');
| Linux | macOS | Windows | |
|---|---|---|---|
app.data |
~/.local/share/fern-notes |
~/Library/Application Support/Lexmata/Fern Notes |
%APPDATA%\Lexmata\Fern Notes |
app.config |
~/.config/fern-notes |
~/Library/Application Support/Lexmata/Fern Notes |
%APPDATA%\Lexmata\Fern Notes |
app.cache |
~/.cache/fern-notes |
~/Library/Caches/Lexmata/Fern Notes |
%LOCALAPPDATA%\Lexmata\Fern Notes\Cache |
app.logs |
~/.local/state/logs/fern-notes |
~/Library/Logs/Lexmata/Fern Notes |
%LOCALAPPDATA%\Lexmata\Fern Notes\Logs |
organization is ignored on the XDG platforms (Linux, the BSDs, Fuchsia),
where the convention is a single lowercase directory named after the
application. Windows has no cache or log
Known Folder, so those two get a subdirectory below the application's own
directory there — otherwise they would collide with app.state.
Application names are sanitized: illegal characters are stripped, Windows
reserved device names (CON, NUL, ...) are escaped, and a name can never
contribute more than one path segment or escape its base directory.
What is resolved
| Getter | Linux / BSD | macOS | Windows |
|---|---|---|---|
home |
$HOME |
$HOME, else /Users/$USER (or /var/root) |
%USERPROFILE%, else %HOMEDRIVE%%HOMEPATH% |
dataHome |
$XDG_DATA_HOME, else ~/.local/share |
~/Library/Application Support |
%APPDATA% |
localDataHome |
same as dataHome |
same as dataHome |
%LOCALAPPDATA% |
configHome |
$XDG_CONFIG_HOME, else ~/.config |
~/Library/Application Support |
%APPDATA% |
cacheHome |
$XDG_CACHE_HOME, else ~/.cache |
~/Library/Caches |
%LOCALAPPDATA% |
stateHome |
$XDG_STATE_HOME, else ~/.local/state |
~/Library/Application Support |
%LOCALAPPDATA% |
binHome |
$XDG_BIN_HOME (non-standard extension), else ~/.local/bin |
~/Applications (application bundles, not on PATH) |
%LOCALAPPDATA%\Programs |
preferencesDir |
= configHome |
~/Library/Preferences |
= configHome |
logHome |
~/.local/state/logs |
~/Library/Logs |
%LOCALAPPDATA% |
runtimeDir |
$XDG_RUNTIME_DIR or null |
null |
null |
tempDir |
$TMPDIR, else /tmp |
$TMPDIR, else /tmp |
%TEMP%, else %TMP%, else %LOCALAPPDATA%\Temp |
dataDirs |
$XDG_DATA_DIRS |
/Library/Application Support |
%ProgramData% |
configDirs |
$XDG_CONFIG_DIRS |
/Library/Preferences |
%ProgramData% |
fontsDir |
~/.local/share/fonts |
~/Library/Fonts |
%LOCALAPPDATA%\Microsoft\Windows\Fonts |
User media — desktopDir, documentsDir, downloadsDir, musicDir,
picturesDir, videosDir, templatesDir, publicShareDir — hangs off the
home directory on macOS, and on Windows for everything except templatesDir
(%APPDATA%\Microsoft\Windows\Templates) and publicShareDir (%PUBLIC%).
On Linux the exported XDG_*_DIR variables win, then
$XDG_CONFIG_HOME/user-dirs.dirs (so localized names like ~/Bureau resolve
correctly), then ~/Desktop and friends.
runtimeDir is null wherever the platform has no such concept; fall back to
tempDir.
A value that is not an absolute path is ignored, as if the variable were unset
— the XDG specification requires this for XDG_*, and the same rule is applied
to every platform's directory variables, since a relative value would resolve
against the process's working directory instead of the user's profile. On
Windows "absolute" means a drive root (C:\) or a UNC prefix (\\server);
a drive-relative C:Users\ada is rejected. Every returned path has its
trailing separator stripped, so results compare and compose predictably.
These directories are not always distinct
Not every platform separates these concepts:
| Collapses to one directory | |
|---|---|
| Windows | configHome = dataHome; cacheHome = stateHome = logHome = localDataHome |
| macOS | dataHome = configHome = stateHome |
Never assume two of them differ — in particular, never recursively delete one
to "reset" a single category. AppPaths does keep an application's cache and
logs in their own subdirectories on every platform.
Android, iOS, and unknown platforms
These platforms hand out sandboxed paths at runtime, so nothing usable can be
derived from the environment. Getters that resolve a single directory throw
OsPathsException there; dataDirs/configDirs are empty and runtimeDir is
null, because a sandbox has no machine-wide directories. Wire the real
directories once at startup, and the rest of the API works unchanged:
import 'package:path_provider/path_provider.dart';
Future<void> initPaths() async {
// needsExplicitPaths, not isMobile: it also covers OsFamily.unknown, which
// would otherwise be left unconfigured.
if (!OsFamily.current.needsExplicitPaths) return;
OsPaths.instance = ExplicitPaths(
family: OsFamily.current,
home: (await getApplicationDocumentsDirectory()).path,
dataHome: (await getApplicationSupportDirectory()).path,
cacheHome: (await getTemporaryDirectory()).path,
documentsDir: (await getApplicationDocumentsDirectory()).path,
);
}
Isolates do not share
OsPaths.instance. Dart statics are per-isolate, so a resolver installed on the main isolate is invisible insideIsolate.spawnor Flutter'scompute()— a background isolate rebuilds the host resolver and every getter throws. Install one at the top of the isolate's entry point, or pass the directories you need across the boundary.
Testing
Every platform's behaviour can be exercised from any host by supplying a family and an environment:
final paths = OsPaths(
family: OsFamily.windows,
environment: {'USERPROFILE': r'C:\Users\ada', 'APPDATA': r'C:\Users\ada\AppData\Roaming'},
);
expect(paths.app('Fern Notes').config, r'C:\Users\ada\AppData\Roaming\Fern Notes');
Reading user-dirs.dirs goes through an injectable FileReader, so tests never
touch the disk. Set OsPaths.instance to swap the resolver globally, and use
addTearDown(OsPaths.resetInstance) to restore the default.
Caveats
- Windows Known Folders that the user has relocated (Documents redirected into
OneDrive, for example) are not detected — that needs
SHGetKnownFolderPath, which would require a plugin. Override the affected directory explicitly:ExplicitPaths(documentsDir: ..., ...). - No directory is created unless you call one of the
ensure*methods. Those throwFileSystemException(adart:ioerror, notOsPathsException) when creation fails, and they follow symbolic links — resolve the result and compare it against the expected base if that is part of your threat model. tempDirfalls back to/tmpwhen$TMPDIRis unset./tmpis shared and world-writable, andAppPaths.tempunder it is a predictable name; useDirectory.systemTemp.createTemp()when you need a private directory.- The web is not supported: this package imports
dart:io, which does not compile for web targets. parseUserDirsfollows the reference implementation and silently ignores entries it cannot resolve (relative values,~,${HOME}, unterminated quotes), falling back to the English defaults.user-dirs.dirsis read once per resolver and cached, including a miss. Construct a newOsPathsto pick up later changes.
Conformance
- Relative values in environment variables are ignored, as the XDG specification requires, rather than returned as-is.
- Windows
cacheHome/logHomeare%LOCALAPPDATA%— there is no cache or log Known Folder — andAppPathsnestsCacheandLogsbelow the application directory so they stay distinct fromstate. - Application names are sanitized so they cannot escape their base directory, contribute more than one path segment, or collide with a Windows reserved device name.
user-dirs.dirsparsing follows thexdg-user-dirsreference implementation, including its backslash escapes and its silent skipping of entries that cannot be resolved.
License
MIT — see LICENSE.
Copyright (c) 2026 Joseph R. Quinn.
Libraries
- os_paths
- OS-specific standard directories for Flutter and Dart applications.