Smarter platform-dir routing, a platformdirs-based skill, and an AI-age framing for config2py
Summary
The current user-data-folder guidance (and config2py.AppData's implicit assumptions) covers one shape well — per-user Python apps on Linux — but is incomplete in three ways:
- Routing logic only branches one way. It always recommends XDG. It doesn't ask whether the package is shipping as a system daemon (FHS), a per-user app (XDG), or a library that should defer placement to its caller.
- It under-uses
platformdirs. platformdirs is the de-facto Python standard for cross-OS path resolution (used by pip, poetry, black, etc.). It already handles macOS (~/Library/Application Support/...), Windows (%APPDATA% / %LOCALAPPDATA%), and the four XDG categories — including XDG_STATE_HOME, which AppData currently collapses into "data".
- The framing predates AI agents.
config2py was originally about reducing boilerplate around config CRUD. A lot of that boilerplate is now trivially generated by an AI agent on demand. We should be honest with users about when adopting the dependency is still worth it and when it isn't.
This issue proposes three concrete deliverables:
- (A) Extend
config2py.AppData (or add a new helper) with a routing-aware API.
- (B) Add a
platformdirs-based skill at .claude/skills/<name>/ so AI agents have the canonical answer when the dependency on config2py isn't warranted.
- (C) Add a "When to use
config2py.AppData vs. platformdirs directly" note to the README.
A naming question (item D) for the skill is at the bottom.
(A) Routing logic gaps
AppData today resolves to XDG by default and seeds-on-missing — which is correct for a per-user Python package. But for a package {pkg} that may be deployed in different shapes, the right base directory differs:
| Deployment shape |
Config |
Persistent state |
Logs |
| Per-user CLI / library on user's laptop |
~/.config/{pkg}/ |
~/.local/share/{pkg}/ |
~/.local/state/{pkg}/ |
| System daemon running as root or service user (systemd, sysvinit) |
/etc/{pkg}/ |
/var/lib/{pkg}/ |
/var/log/{pkg}/ |
| Tied to a specific repo / project workspace |
<repo>/config.toml |
<repo>/.{pkg}/ (gitignored) |
journal/stderr |
Today's AppData quietly assumes row 1. For a system daemon, it would route data to /root/.local/share/{pkg}/, which is awkward (root owning XDG dirs) and brittle (everything moves if you switch to a {pkg} service user).
Additional XDG gap: XDG_STATE_HOME (~/.local/state/) is missing. The spec deliberately distinguishes:
- data (
~/.local/share/): user-curated, "would be sad to lose" content.
- state (
~/.local/state/): runtime artifacts — logs, history, append-only journals, recently-used lists. Regenerable in spirit, but not pure cache.
For applications that emit append-only event logs (decisions.jsonl, signals.jsonl, etc.), state is the textbook fit, not data.
Proposed API sketch
from config2py import AppData
# Default — per-user, XDG (Linux), Library (macOS), AppData (Windows)
app = AppData("{pkg}")
# System daemon — FHS
app = AppData("{pkg}", scope="system")
# config_dir() -> /etc/{pkg}/
# data_dir() -> /var/lib/{pkg}/
# state_dir() -> /var/log/{pkg}/ (or /var/lib/{pkg}/state/)
# Project-local — for tools tied to a repo
app = AppData("{pkg}", scope="project", root="/path/to/repo")
# Override hook — single env var that clobbers everything (for systemd units)
app = AppData("{pkg}", env_override="{PKG}_DATA_DIR")
The four directory kinds (config_dir, data_dir, state_dir, cache_dir) all exist in both XDG and FHS; only the base path differs.
(B) A platformdirs-based skill
The current user-data-folder skill is good in scope but uses config2py as the implementation. Many users — especially those scaffolding a small project with an AI agent — don't need or want a config2py dependency just to compute three paths and copy a seed file.
Recommendation:
- Add a new skill at
i2mint/config2py/.claude/skills/<name>/SKILL.md that uses platformdirs directly. Sketch below.
- Optionally keep the current skill (renamed to
config2py-user-data-folder or similar) for users who do want the AppData ergonomics — config CRUD, lazy resource loaders, the seed/artifact split. But this might be redundant; worth deciding.
- The new skill should explicitly state when each option makes sense (see (C) below).
Sketch of the platformdirs-based skill
---
name: <name>
description: Set up per-user persistent data directories for a Python package.
Use platformdirs for OS-correct paths (XDG / macOS / Windows). Triggers on
questions about config dirs, data dirs, app folders, or where to put
user-editable resources.
---
# Platform Directories for Python Packages
## Decision: which scope?
Before picking paths, decide what kind of package this is:
| Shape | Scope | Use |
|---|---|---|
| CLI, library, GUI app run by a user | per-user | `platformdirs` (this skill) |
| Daemon running as root / service user | system | FHS — `/etc/{pkg}/`, `/var/lib/{pkg}/` |
| Tool tied to a single repo workspace | project | files inside the repo (gitignored) |
This skill covers the first row. For systemd services, prefer FHS conventions
and let the unit file inject the paths via env var.
## Layout (per-user)
`platformdirs` resolves these correctly per-OS:
| Kind | Linux (XDG) | macOS | Windows |
|---|---|---|---|
| config | `~/.config/{pkg}/` | `~/Library/Application Support/{pkg}/` | `%APPDATA%\{pkg}\` |
| data | `~/.local/share/{pkg}/` | `~/Library/Application Support/{pkg}/` | `%LOCALAPPDATA%\{pkg}\` |
| state | `~/.local/state/{pkg}/` | `~/Library/Application Support/{pkg}/` | `%LOCALAPPDATA%\{pkg}\` |
| cache | `~/.cache/{pkg}/` | `~/Library/Caches/{pkg}/` | `%LOCALAPPDATA%\{pkg}\Cache\` |
Use **state** for append-only logs, history, recently-used lists.
Use **data** for content the user would be upset to lose.
Use **cache** only for content that can be regenerated from scratch.
## Implementation
### 1. Add dependency
```toml
# pyproject.toml
dependencies = ["platformdirs>=4"]
2. Resolver module
# {pkg}/_paths.py
from __future__ import annotations
import os
from pathlib import Path
from functools import cache
from platformdirs import PlatformDirs
_APP = "{pkg}"
_ENV_OVERRIDE = "{PKG}_DATA_DIR" # single env var clobbers everything
@cache
def _dirs() -> PlatformDirs:
return PlatformDirs(appname=_APP, appauthor=False)
def _override() -> Path | None:
raw = os.environ.get(_ENV_OVERRIDE)
return Path(raw).expanduser() if raw else None
def config_dir() -> Path:
p = _override() / "config" if _override() else Path(_dirs().user_config_dir)
p.mkdir(parents=True, exist_ok=True)
return p
def data_dir() -> Path:
p = _override() / "data" if _override() else Path(_dirs().user_data_dir)
p.mkdir(parents=True, exist_ok=True)
return p
def state_dir() -> Path:
p = _override() / "state" if _override() else Path(_dirs().user_state_dir)
p.mkdir(parents=True, exist_ok=True)
return p
def cache_dir() -> Path:
p = _override() / "cache" if _override() else Path(_dirs().user_cache_dir)
p.mkdir(parents=True, exist_ok=True)
return p
3. Seed-on-missing (only if you ship reference data)
# {pkg}/_seed.py
from __future__ import annotations
from importlib.resources import files
from pathlib import Path
from {pkg}._paths import config_dir, data_dir
def ensure_seeded(name: str, *, kind: str = "config") -> Path:
"""Copy {pkg}/_seed_data/{kind}/{name} into user dir if missing. Never overwrites."""
target_root = config_dir() if kind == "config" else data_dir()
target = target_root / name
if target.exists():
return target
src = files(f"{pkg}._seed_data.{kind}").joinpath(name)
target.write_bytes(src.read_bytes())
return target
4. Anti-patterns (carried over from the original skill)
- Don't seed at import time — seed lazily on first access.
- Don't overwrite user-edited files when re-seeding.
- Don't use
__file__ to locate seed data — use importlib.resources.
- Don't store large binaries as seeds.
- Don't hardcode
~/.config/... — let platformdirs decide per-OS.
---
## (C) When to depend on config2py vs. just write the boilerplate
`config2py` was designed in the pre-AI-agent era to remove repetitive config-CRUD boilerplate. That value proposition has shifted: an AI agent can generate a 30-line `_paths.py` like the one above on demand, with no runtime dependency.
Suggested README note (rough draft):
> **Do you actually need `config2py`?**
>
> `config2py` shines when:
>
> - You want **opinionated config CRUD** (load → mutate → save) across multiple stores (env, file, secrets manager) with one API.
> - You want the **resource/artifact split** baked in — `get_resource("foo.json")` semantics with seed-on-missing.
> - You're maintaining **multiple packages** that should look and feel the same — adopting `AppData` once gives you that consistency for free.
> - Your users will read your code and benefit from a recognizable abstraction.
>
> Reach for `platformdirs` directly (no `config2py`) when:
>
> - You just need to compute and create three paths.
> - You want zero runtime dependencies beyond the standard library + one well-known shim.
> - You're scaffolding with an AI agent that can generate the ~30 lines of glue inline.
> - Your project is small and the `AppData` abstraction would be more conceptual overhead than the boilerplate it replaces.
>
> If you're hesitating, `platformdirs` is the safer default. You can always migrate to `config2py.AppData` later — the directory layout it produces is compatible.
This framing is honest and probably *increases* trust in `config2py` rather than decreasing it.
---
## (D) Naming
`user-data-folder` is descriptive but slightly misleading:
- It covers more than "data" — config, cache, state too.
- "User" is misleading once we add system / project scopes.
Candidates:
- **`platform-dirs`** — matches the underlying library and standard term. Clear, but couples the skill name to one implementation choice.
- **`app-dirs`** — generic, OS-agnostic phrasing. Nice and short.
- **`op-dirs`** — opaque without context; would need explanation.
- **`package-paths`** — explicit about scope (Python package paths), neutral on OS.
My weak preference: **`app-dirs`** for the platformdirs-based skill (general-purpose), keeping `user-data-folder` as a config2py-flavored alias if we keep both. But happy to bikeshed.
---
## Proposed next steps
- [ ] Decide on routing API for `AppData` (item A) — accept this issue's sketch or counter-propose.
- [ ] Add `XDG_STATE_HOME` support to `AppData` regardless of (A).
- [ ] Add the `platformdirs`-based skill to `.claude/skills/` (item B). Open question: keep current skill as a `config2py`-flavored variant, or replace?
- [ ] Add the "when to use" framing to README (item C).
- [ ] Pick a name (item D).
Smarter platform-dir routing, a platformdirs-based skill, and an AI-age framing for config2py
Summary
The current
user-data-folderguidance (andconfig2py.AppData's implicit assumptions) covers one shape well — per-user Python apps on Linux — but is incomplete in three ways:platformdirs.platformdirsis the de-facto Python standard for cross-OS path resolution (used by pip, poetry, black, etc.). It already handles macOS (~/Library/Application Support/...), Windows (%APPDATA%/%LOCALAPPDATA%), and the four XDG categories — includingXDG_STATE_HOME, whichAppDatacurrently collapses into "data".config2pywas originally about reducing boilerplate around config CRUD. A lot of that boilerplate is now trivially generated by an AI agent on demand. We should be honest with users about when adopting the dependency is still worth it and when it isn't.This issue proposes three concrete deliverables:
config2py.AppData(or add a new helper) with a routing-aware API.platformdirs-based skill at.claude/skills/<name>/so AI agents have the canonical answer when the dependency onconfig2pyisn't warranted.config2py.AppDatavs.platformdirsdirectly" note to the README.A naming question (item D) for the skill is at the bottom.
(A) Routing logic gaps
AppDatatoday resolves to XDG by default and seeds-on-missing — which is correct for a per-user Python package. But for a package{pkg}that may be deployed in different shapes, the right base directory differs:~/.config/{pkg}/~/.local/share/{pkg}/~/.local/state/{pkg}//etc/{pkg}//var/lib/{pkg}//var/log/{pkg}/<repo>/config.toml<repo>/.{pkg}/(gitignored)stderrToday's
AppDataquietly assumes row 1. For a system daemon, it would route data to/root/.local/share/{pkg}/, which is awkward (root owning XDG dirs) and brittle (everything moves if you switch to a{pkg}service user).Additional XDG gap:
XDG_STATE_HOME(~/.local/state/) is missing. The spec deliberately distinguishes:~/.local/share/): user-curated, "would be sad to lose" content.~/.local/state/): runtime artifacts — logs, history, append-only journals, recently-used lists. Regenerable in spirit, but not pure cache.For applications that emit append-only event logs (
decisions.jsonl,signals.jsonl, etc.),stateis the textbook fit, notdata.Proposed API sketch
The four directory kinds (
config_dir,data_dir,state_dir,cache_dir) all exist in both XDG and FHS; only the base path differs.(B) A
platformdirs-based skillThe current
user-data-folderskill is good in scope but usesconfig2pyas the implementation. Many users — especially those scaffolding a small project with an AI agent — don't need or want aconfig2pydependency just to compute three paths and copy a seed file.Recommendation:
i2mint/config2py/.claude/skills/<name>/SKILL.mdthat usesplatformdirsdirectly. Sketch below.config2py-user-data-folderor similar) for users who do want theAppDataergonomics — config CRUD, lazy resource loaders, the seed/artifact split. But this might be redundant; worth deciding.Sketch of the platformdirs-based skill
2. Resolver module
3. Seed-on-missing (only if you ship reference data)
4. Anti-patterns (carried over from the original skill)
__file__to locate seed data — useimportlib.resources.~/.config/...— letplatformdirsdecide per-OS.