Skip to content

feat(config): opt-in preserve_tilde_on_save for dotfiles-friendly path serialization#155

Merged
runkids merged 1 commit into
runkids:mainfrom
iFwu:fix/preserve-tilde-in-config
May 14, 2026
Merged

feat(config): opt-in preserve_tilde_on_save for dotfiles-friendly path serialization#155
runkids merged 1 commit into
runkids:mainfrom
iFwu:fix/preserve-tilde-in-config

Conversation

@iFwu
Copy link
Copy Markdown
Contributor

@iFwu iFwu commented May 14, 2026

Problem

When a user shares their skillshare config via dotfiles (or any other cross-machine sync), every cfg.Save() call rewrites ~/... paths into absolute /home/<user>/... form. The user-name (and OS-specific home prefix) gets burned into the config, breaking portability and creating noisy diffs on every operation that touches a target.

Repro:

$ cat ~/.config/skillshare/config.yaml
source: ~/dotfiles/skills
targets:
  claude:
    skills: { path: ~/.claude/skills }

$ skillshare target remove some-other-target
$ cat ~/.config/skillshare/config.yaml
source: /home/alice/dotfiles/skills           # ← expanded
targets:
  claude:
    skills: { path: /home/alice/.claude/skills }  # ← expanded

Load() expands ~ (correctly — os.Stat etc. don't understand ~), but Save() writes the expanded value verbatim, losing the original form.

Proposal

Add an opt-in config flag, default false (no behavior change for existing users):

preserve_tilde_on_save: true

When enabled, Save() folds $HOME prefixes back to ~ before serialization. The in-memory Config is left untouched; we marshal a shallow copy with rewritten path fields. Downstream consumers (sync, validators, etc.) are unaffected.

Folded fields mirror the existing expandPath() calls in Load():

  • cfg.Source, cfg.ExtrasSource
  • cfg.Targets[*].Path, Skills.Path, Agents.Path
  • cfg.Extras[*].Source, Extras[*].Targets[*].Path

FoldHomePath() only folds when:

  • The path is strictly under $HOME (home + separator prefix, or exact home)
  • The path doesn't already start with ~
  • os.UserHomeDir() succeeds

Non-home absolute paths (/opt/foo, /tmp/bar) and relative paths are passed through unchanged. Windows paths fold via PathHasPrefix (case-insensitive on Windows).

Tests

  • internal/utils/path_test.go: 7 cases (exact home, child, sibling-non-home, unrelated-abs, relative, empty, already-tilde) + Windows case-fold test.
  • internal/config/config_tilde_fold_test.go:
    • Default (flag absent) keeps absolute paths.
    • Flag enabled folds all six path fields.
    • Non-home absolute paths are not folded.
    • In-memory Config is not mutated.
    • Save→Load→Save is byte-for-byte idempotent.

All internal/... tests pass on the branch.

Backward compatibility

Zero. Default false. Existing configs and CI behave identically.

Notes

  • I've been running this patch on my own setup for daily dotfiles sync — it makes the config diff stable across my Linux VM, macOS, and Windows machines.
  • Happy to rebase / adjust naming / split commits as you prefer.

Copy link
Copy Markdown

@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 introduces an opt-in PreserveTildeOnSave configuration setting that folds absolute home directory paths back to the ~ form during YAML serialization, ensuring machine-agnostic configuration files. The implementation includes a new FoldHomePath utility and logic to clone and process configuration fields before saving. Feedback suggests including the missing AgentsSource field in the folding logic and optimizing performance by caching the result of os.UserHomeDir() instead of calling it repeatedly during serialization.

Comment thread internal/config/config.go Outdated
Comment on lines +547 to +548
out.Source = utils.FoldHomePath(out.Source)
out.ExtrasSource = utils.FoldHomePath(out.ExtrasSource)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The AgentsSource field is missing from the path folding logic in cloneForSave. For consistency with Source and ExtrasSource, it should also be folded back to the ~ form when PreserveTildeOnSave is enabled.

Suggested change
out.Source = utils.FoldHomePath(out.Source)
out.ExtrasSource = utils.FoldHomePath(out.ExtrasSource)
out.Source = utils.FoldHomePath(out.Source)
out.AgentsSource = utils.FoldHomePath(out.AgentsSource)
out.ExtrasSource = utils.FoldHomePath(out.ExtrasSource)

Comment thread internal/utils/path.go
if path == "" || HasTildePrefix(path) {
return path
}
home, err := os.UserHomeDir()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Calling os.UserHomeDir() repeatedly inside a loop (as done in cloneForSave) can be inefficient, especially on Windows where it involves syscalls. Consider retrieving the home directory once in the caller and passing it to FoldHomePath, or caching the result within the utils package to improve performance during serialization.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 772054f67c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread internal/config/config.go
// config to YAML. Useful when the config is shared via dotfiles across
// machines or users. The in-memory config is unaffected; Load() still
// expands ~ as usual.
PreserveTildeOnSave bool `yaml:"preserve_tilde_on_save,omitempty"`
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Add new config key to published JSON schema

Introducing preserve_tilde_on_save in Config without updating schemas/config.schema.json makes saved configs invalid against the advertised schema (additionalProperties: false at the root), so editors and any schema-based validation will reject this new key even though the app now writes/reads it. Since Save() always emits the schema directive comment, users enabling this option will immediately see schema errors.

Useful? React with 👍 / 👎.

Add PreserveTildeOnSave config flag (default false). When enabled, the
Save path folds $HOME prefixes back to ~ when serializing the in-memory
Config to YAML so the on-disk config stays portable across machines
(e.g. when shared via dotfiles).

The in-memory Config is left untouched; cloneForSave() produces a
shallow copy with path fields rewritten. Mirrors the existing
expandPath() calls performed in Load():
  - cfg.Source, cfg.ExtrasSource
  - cfg.Targets[*].Path / Skills.Path / Agents.Path
  - cfg.Extras[*].Source and Extras[*].Targets[*].Path

Adds:
  - utils.FoldHomePath() helper with unit tests (exact-home, child,
    sibling-non-home, unrelated-abs, relative, Windows case-insensitive)
  - 3 Save-path tests covering default-off, fold-on, and Load→Save
    idempotency.

Default off keeps the change backward compatible: existing users see no
behavior difference. Users who share their config via dotfiles can opt
in with:

    preserve_tilde_on_save: true
@iFwu iFwu force-pushed the fix/preserve-tilde-in-config branch from 772054f to 29a48f4 Compare May 14, 2026 04:42
@iFwu
Copy link
Copy Markdown
Contributor Author

iFwu commented May 14, 2026

Addressed all three review comments in the latest force-push (29a48f40):

  1. AgentsSource missing from fold — Added. cloneForSave() now folds Source, AgentsSource, and ExtrasSource.
  2. os.UserHomeDir() called repeatedly — Refactored: home is resolved once in cloneForSave() and passed to a new FoldHomePathWith(path, home) helper via a local closure. The zero-arg FoldHomePath() remains for callers that fold a single path.
  3. Schema missing preserve_tilde_on_save — Added to schemas/config.schema.json with type boolean, default false, and description.

All internal/... tests pass.

@runkids
Copy link
Copy Markdown
Owner

runkids commented May 14, 2026

Thanks! I'll handle docs update on our side after merge.

@runkids runkids merged commit 5403595 into runkids:main May 14, 2026
6 of 8 checks passed
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.

2 participants