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 inside Isolate.spawn or Flutter's compute() — 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 throw FileSystemException (a dart:io error, not OsPathsException) 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.
  • tempDir falls back to /tmp when $TMPDIR is unset. /tmp is shared and world-writable, and AppPaths.temp under it is a predictable name; use Directory.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.
  • parseUserDirs follows the reference implementation and silently ignores entries it cannot resolve (relative values, ~, ${HOME}, unterminated quotes), falling back to the English defaults.
  • user-dirs.dirs is read once per resolver and cached, including a miss. Construct a new OsPaths to pick up later changes.

Conformance

  • Relative values in environment variables are ignored, as the XDG specification requires, rather than returned as-is.
  • Windows cacheHome/logHome are %LOCALAPPDATA% — there is no cache or log Known Folder — and AppPaths nests Cache and Logs below the application directory so they stay distinct from state.
  • 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.dirs parsing follows the xdg-user-dirs reference 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.