Skip to content

feat(security): block hidden paths in all vault operations - #428

Merged
aliasunder merged 19 commits into
mainfrom
worktree-block-hidden-paths
Aug 13, 2026
Merged

feat(security): block hidden paths in all vault operations#428
aliasunder merged 19 commits into
mainfrom
worktree-block-hidden-paths

Conversation

@aliasunder

@aliasunder aliasunder commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Summary

Hidden paths (any dot-prefixed segment — .obsidian/, .trash/, dotfiles) were excluded from listings, search, and the file watcher, but explicit paths were still served: vault_read_file would read .obsidian/plugins/<plugin>/data.json directly, and any write tool could create or modify .md files inside hidden folders. Community plugins often store their own third-party API keys in data.json files, so a leaked MCP token also exposed those keys. Reported in #427.

This PR blocks hidden paths in every client-facing vault operation, matching Obsidian, which ignores dot-prefixed paths entirely.

Changes

  • Shared predicate src/utils/has-hidden-path-segment.ts — consolidates three previously duplicated inline implementations (listing filter, file watcher, index rebuild), so the definition of "hidden" can't drift between layers.
  • Guard in resolveSafePath — after the traversal check, the resolved vault-relative path is rejected if any segment is dot-prefixed. Checking the resolved path means notes/./plan.md and a/../b.md normalize cleanly while a/../.obsidian/x is caught. The guard fires before any filesystem access — it rejects on path shape alone and reveals nothing about what exists. Every read, write, patch, move, delete, and task update routes through it.
  • Internal config readers unaffecteddaily-notes.ts and task-format-config.ts read .obsidian/ config via direct readFile and deliberately bypass the guard.
  • Memory file names reject leading dots — memory paths are built via join (not resolveSafePath), so memoryFilePath gains its own check: a name like .secret would have created a hidden, unindexed file.
  • DocsErrors: bullets on every affected tool description, SECURITY.md (new Hidden paths section + database-placement note), README data-integrity bullet, ARCHITECTURE.md path-safety section.

Behavior changes

Previously-succeeding calls that now return hidden path blocked: "<path>" targets a hidden file or folder:

  • vault_read_note (all modes) / vault_read_file on paths inside hidden folders
  • vault_write_note, vault_patch_note, vault_replace_in_note, vault_delete_span, vault_update_properties, vault_delete_note, vault_update_task on hidden paths
  • vault_move_note with a hidden old_path or new_path (previously a note could be moved into .obsidian/ and vanish from the index)
  • vault_list_notes / vault_list_files with an explicitly hidden folder (previously returned an empty list; an explicit error beats a silent [] that reads as "folder is empty")
  • vault_update_memory / vault_get_memory with a dot-prefixed file name

Graph and search tools are unchanged — they query the index, which has always excluded hidden paths.

Tests

  • Unit spec for the shared predicate (9 cases including interior-dot and trailing-dot names).
  • Guardrail tests for every surface (18 new tests): hidden fixtures are created on disk, so removing the guard makes the operations succeed and the tests fail for that reason — not via a coincidental "not found". Write/delete/patch rejections also assert the file was not created or modified.
  • Normalization pins: a/../.obsidian/… rejected; notes/./plan.md and notes/version.2/file.md accepted with content returned.
  • Mutation-verified both directions: deleting the guard fails exactly the 17 resolveSafePath guardrail tests (because the calls succeed); weakening the predicate to includes(".") fails the accepted-path tests.

Verification

npm run lint (0 errors), npm run build, npm test (2484 passed). DOCKERHUB.md regeneration produced no diff (the changed README section is not part of it).

Refs #427

BREAKING CHANGE: paths containing dot-prefixed segments (.obsidian/, .trash/, dotfiles) are now rejected by all vault read, write, move, delete, and listing operations. Clients that read or wrote files inside hidden folders must stop, or the operator should relocate that content into visible folders. This matches Obsidian, which does not surface hidden paths at all.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Security

    • Blocked access to hidden dot-prefixed files and folders across vault reads, writes, edits, moves, deletes, listings, watching, and indexing.
    • Prevented creation or access of hidden memory files.
    • Continued allowing visible names that merely contain dots and safe relative paths.
  • Documentation

    • Updated usage, architecture, and security guidance to describe hidden-path protections and related storage recommendations.
  • Tests

    • Added coverage confirming blocked operations leave existing files unchanged and do not create hidden files.

aliasunder and others added 2 commits August 12, 2026 21:13
Shared hasHiddenPathSegment predicate (consolidates three duplicate
inline implementations); resolveSafePath rejects any dot-prefixed
segment on the resolved vault-relative path before filesystem access;
memory file names reject leading dots (memory paths bypass
resolveSafePath via direct join). Guardrail tests for every read and
write surface.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rity docs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread src/vault-mcp/vault-operations/vault-filesystem.ts
Comment thread src/vault-mcp/vault-operations/memory-store.ts
@umm-actually

umm-actually Bot commented Aug 13, 2026

Copy link
Copy Markdown

umm-actually re-reviewed at bcb0be5

1 new finding(s) posted (8 tracked finding(s) across all runs).

Context notes
  • Priority docs not included: server.json, docker-compose.yml, .env.example (missing, unreadable, or over budget)
  • 12 related file(s) excluded by max_related_files cap: src/vault-mcp/vault-operations/note-mover.ts, src/vault-mcp/vault-operations/task-updater.ts, src/vault-mcp/vault-operations/vault-patcher.ts, src/vault-mcp/mcp-core/__tests__/mcp-router.test.ts, src/vault-mcp/mcp-core/__tests__/prompt-test-harness.ts, src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts, src/vault-mcp/search/__tests__/memory-index.test.ts, src/vault-mcp/search/__tests__/memory-recall.test.ts, src/vault-mcp/search/__tests__/search-helpers.test.ts, src/vault-mcp/search/__tests__/search-index.test.ts, src/vault-mcp/search/__tests__/task-queries.test.ts, src/vault-mcp/vault-operations/__tests__/asset-operations.test.ts

umm-actually · deepseek/deepseek-v4-flash-0731

aliasunder and others added 4 commits August 12, 2026 21:24
vault_delete_memory goes through memoryFilePath() which now rejects
dot-prefixed names, but the tool description's Errors section was
missing this error — unlike vault_get_memory and vault_update_memory
which both had it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The test used a substring match (toThrow("hidden path blocked")) on a
deterministic error message. Replace with the exact message including
the original input path, matching the project's assertion conventions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…y surfaces

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@umm-actually

umm-actually Bot commented Aug 13, 2026

Copy link
Copy Markdown

Pre-existing: new-directory rescan tests accept a condition where hasHiddenPathSegment is not the rescan's filter
Medium severity · correctness · high confidence

src/vault-mcp/search/__tests__/file-watcher.test.ts:305 — beyond the diff's line ranges, in code the changes touch or depend on.

The rescan tests in the new-directory rescan suite use the watcher's real addDir handler and report their own getWatched() result. But hasHiddenPathSegment is invoked only inside rescanNewDirectory, not in the chokidar ignored callback. In these tests, a hidden-path fixture (e.g. .trash/inside.md) created inside a newly-added directory would pass the rescan's ignored check (since the fake watcher doesn't enforce it), enter handleChange via the rescan, and be indexed — the test could pass with hidden files being indexed. This doesn't match the actual code (the real chokidar's ignored filter prevents them from reaching the rescan at all, and the rescan's own filter is a second line of defense), but the test could pass against a buggy future implementation that drops the filter. This is the same family of wrong-reason pass as the prior bot comment about missing updateMemory/deleteMemory tests.

Failure scenario: A developer removes the if (hasHiddenPathSegment(relativePath)) continue line from rescanNewDirectory, assuming chokidar's ignored callback already handles it. The existing rescan tests still pass (they seed only visible files), so the regression is not caught by CI. In production, a hidden .obsidian/plugins/plugin/data.json file created inside a brand-new directory during the race window would be indexed, exposing plugin API keys to full-text search.

Suggested fix
Add a test (or assertion in an existing test) that seeds a hidden file inside a newly-added directory and confirms the rescan does NOT index it — specifically that `upsertNote` is never called for the hidden path. This proves the hidden-segment filter inside rescanNewDirectory actually excludes entries, not just that the chokidar ignored callback does.

@umm-actually

umm-actually Bot commented Aug 13, 2026

Copy link
Copy Markdown

Pre-existing: deduplication of duplicate add events is untested for the serialization guard
Low severity · correctness · medium confidence

src/vault-mcp/search/__tests__/file-watcher.test.ts:385 — beyond the diff's line ranges, in code the changes touch or depend on.

The file-watcher code in handleChange serializes embedding per path via a pendingEmbeds map to prevent interleaving — when two chokidar events arrive for the same note, the second waits for the first's embedding to finish. The .catch() on previousEmbed swallows a previous embedding failure so a transient error doesn't permanently block the path. No test exercises this serialization path, meaning a future awaitWriteFinish change or handleChange refactor that drops the serialization guard would pass CI but silently interleave embedding writes, potentially overwriting fresh vectors with stale content from the previous event. The guard was introduced in a prior PR (the pendingEmbeds map and its .catch() handler), but its correctness-gating behavior — preventing a real data-corruption path — is untestable without a test that deliberately fires duplicate events.

Failure scenario: A developer removes the pendingEmbeds map and the .catch() serialization from handleChange during a refactor. CI passes. In production, a note edited rapidly (save + immediate re-save) triggers two chokidar add events. The second event's handleChange reads the newer file content but its embedNote races the first event's embedding. The first event finishes embedding its stale content AFTER the second, so the vector table stores the stale embedding for the newer text. Search returns stale results for this note until the next edit.

Suggested fix
Add a test that fires duplicate `add` events for the same file path (two rapid chokidar events) and verifies that both embedding calls complete, and that the second call's embedding is the one ultimately stored. Alternatively, mock `embedNote` to track invocation order and verify serialization.

@aliasunder

Copy link
Copy Markdown
Owner Author

Addressing the two umm-actually findings from the ec7bc3c re-review (both PR-level, beyond-diff):

1. "new-directory rescan tests accept a condition where hasHiddenPathSegment is not the rescan's filter" — false positive. The test the suggested fix asks for already exists: "ignores dot-directories during the rescan" (src/vault-mcp/search/__tests__/file-watcher.test.ts:643). It seeds .trash/hidden.md inside a newly-added directory alongside a visible sibling, drives the real rescan through the fake-chokidar harness — whose ignored callback is deliberately not enforced, so the rescan's own hasHiddenPathSegment filter is exactly what's under test — and asserts the hidden note is not indexed (toHaveLength(0)), the visible sibling IS indexed (proving the rescan ran, no silent no-op), and addedPaths contains only the visible file. Removing the filter line from rescanNewDirectory fails this test. The claim that the rescan tests "seed only visible files" is incorrect.

2. "deduplication of duplicate add events is untested for the serialization guard" — valid, pre-existing, tracked. The pendingEmbeds serialization guard predates this PR (this PR's only change to file-watcher.ts is the shared-predicate import swap) and its test needs a proper ordering seam per the repo's no-time-mocking convention. The maintainer dispositioned it as a tracked task on the project board rather than expanding this security PR; the task captures the failure scenario and the suggested test shape.


🔍 ship-check · pr-monitor · claude-fable-5

@aliasunder

Copy link
Copy Markdown
Owner Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The PR adds shared hidden-path detection and blocks dot-prefixed paths across vault filesystem operations, memory files, listings, watching, and indexing. Tests and documentation cover rejection behavior, preserved files, visible dot-containing names, and security boundaries.

Hidden path protection

Layer / File(s) Summary
Path contract and enforcement
src/utils/has-hidden-path-segment.ts, src/vault-mcp/vault-operations/vault-filesystem.ts, src/vault-mcp/vault-operations/memory-store.ts
The shared predicate detects hidden path segments. Vault path resolution and memory filename validation reject hidden targets before filesystem access.
Discovery and indexing integration
src/vault-mcp/search/file-watcher.ts, src/vault-mcp/search/search-index.ts
Listings, file watching, and indexing use the shared hidden-path predicate.
Operation regression coverage
src/vault-mcp/vault-operations/__tests__/*
Tests cover hidden-path rejection across reads, writes, updates, deletes, moves, patching, memory operations, task updates, listings, traversal normalization, and visible dot-containing paths.
Tool contracts and security documentation
AGENTS.md, ARCHITECTURE.md, README.md, SECURITY.md, src/vault-mcp/mcp-core/tools/*
Tool errors and project documentation describe hidden-path blocking and related security rules.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Mergeability Score: 🟠 High · up to ec7bc

Hidden memory files can still be returned through enumeration, allowing dot-prefixed content to be exposed despite the new blocking behavior, and path-validation errors may disclose sensitive user path data. The PR is not merge-ready until these security issues are addressed.

Possibly related PRs

Suggested labels: Review effort 2/5

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: blocking hidden paths across vault operations.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-block-hidden-paths

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/vault-mcp/vault-operations/memory-store.ts`:
- Around line 320-333: Update the no-file enumeration filters in getMemory,
listMemoryFiles, and listMemoryFileNames to exclude .md entries whose basename
starts with a dot, matching the hidden-file restriction enforced by
memoryFilePath. Add regression coverage for each surface confirming dot-prefixed
memory files are omitted while normal memory files remain available.

In `@src/vault-mcp/vault-operations/vault-filesystem.ts`:
- Around line 73-76: Update resolveSafePath so both hidden-path and traversal
violations throw the same stable generic client-facing error without including
notePath, resolved paths, or other implementation details; retain any detailed
path information only in internal diagnostics if already supported.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 78060a5a-33e3-4ae3-9b60-2d736a31b9cf

📥 Commits

Reviewing files that changed from the base of the PR and between 59e460d and ec7bc3c.

📒 Files selected for processing (19)
  • AGENTS.md
  • ARCHITECTURE.md
  • README.md
  • SECURITY.md
  • src/utils/__tests__/has-hidden-path-segment.test.ts
  • src/utils/has-hidden-path-segment.ts
  • src/vault-mcp/mcp-core/tools/asset-tools.ts
  • src/vault-mcp/mcp-core/tools/memory-tools.ts
  • src/vault-mcp/mcp-core/tools/task-tools.ts
  • src/vault-mcp/mcp-core/tools/vault-crud-tools.ts
  • src/vault-mcp/search/file-watcher.ts
  • src/vault-mcp/search/search-index.ts
  • src/vault-mcp/vault-operations/__tests__/memory-store.test.ts
  • src/vault-mcp/vault-operations/__tests__/note-mover.test.ts
  • src/vault-mcp/vault-operations/__tests__/task-updater.test.ts
  • src/vault-mcp/vault-operations/__tests__/vault-filesystem.test.ts
  • src/vault-mcp/vault-operations/__tests__/vault-patcher.test.ts
  • src/vault-mcp/vault-operations/memory-store.ts
  • src/vault-mcp/vault-operations/vault-filesystem.ts

Comment thread src/vault-mcp/vault-operations/memory-store.ts
Comment thread src/vault-mcp/vault-operations/vault-filesystem.ts
A pre-existing dot-prefixed .md in the memory folder leaked through the
all-files read and both list surfaces; filter now mirrors the write-side
rejection in memoryFilePath.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same rationale, fewer words — the shared-definition and resolved-path
constraints stay; restated justification goes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@umm-actually

umm-actually Bot commented Aug 13, 2026

Copy link
Copy Markdown

Scope README Files bullet to visible folders
Low severity · correctness · high confidence

README.md:202 — beyond the diff's line ranges, in code the changes touch or depend on.

README's Files section still says an agent can vault_list_files and Browse/List any folder's files and sizes — after this PR an explicit hidden folder (e.g. list_files({ folder: .attachments })) is rejected with a hidden path error, so the promise is now false. The same PR adds the Data Integrity bullet that says hidden paths are off-limits, so the document is internally contradictory.

Failure scenario: A user with a non-root .obsidian q/* and any custom .attachments directory no longer gets the folder listing it used to; the README's a visible-folder example would direct them straight into an error.

Suggested fix
Rewrite the Files bullet to say 'search any visible folder' or similar.

aliasunder and others added 3 commits August 13, 2026 01:19
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread src/vault-mcp/vault-operations/vault-filesystem.ts
aliasunder and others added 7 commits August 13, 2026 01:25
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Runtime Hardening section describes system properties; the lone
imperative bullet now matches that register.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ync claim

The Files section promised listing any folder — hidden folders now
error, and the Data Integrity bullet says so, making the doc
self-contradictory. SECURITY.md's symlink note loses the Obsidian Sync
sentence: symlink handling under Sync is officially unsupported and
undefined, so the claim wasn't defensible.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@aliasunder

Copy link
Copy Markdown
Owner Author

Addressing the three PR-level findings:

Scope README Files bullet to visible folders — fixed in e05582f: the Browse bullet now reads "list any visible folder's files", and DOCKERHUB.md is regenerated. The contradiction with the Data Integrity bullet is gone.

New-directory rescan tests accept a condition where hasHiddenPathSegment is not the rescan's filter — refuted: the dedicated guardrail test creates a hidden .trash/hidden.md inside a newly-added directory and asserts it is not indexed after the rescan (file-watcher.test.ts, new-directory rescan suite). That test runs against the fake watcher, which does NOT enforce chokidar's ignored — so the rescan's own filter is the only thing excluding the fixture, and removing the filter fails exactly that test. The tests earlier in the suite verify other rescan behaviors and don't need to duplicate the exclusion assertion.

Deduplication of duplicate add events is untested for the serialization guard — acknowledged as a pre-existing gap, deliberately not addressed in this PR: testing the pendingEmbeds serialization properly needs an ordering seam rather than mocked scheduler internals, and it is tracked as follow-up work.

Comment thread README.md Outdated
aliasunder added a commit that referenced this pull request Aug 13, 2026
The server reads exactly two config files — daily-notes.json
(core-plugin-data) and the Tasks plugin's data.json
(community-plugin-data, powering vault_update_task's write-format
detection) — so both categories default on; the desktop's per-device
Vault configuration sync toggles remain the real gate. #428 blocks all
tool reads of .obsidian/, and the deploy docs note plainly that plugin
settings can carry API keys and sit unread on the config volume.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@aliasunder
aliasunder merged commit c1589a0 into main Aug 13, 2026
18 checks passed
@aliasunder
aliasunder deleted the worktree-block-hidden-paths branch August 13, 2026 19:45
aliasunder added a commit that referenced this pull request Aug 13, 2026
The server reads exactly two config files — daily-notes.json
(core-plugin-data) and the Tasks plugin's data.json
(community-plugin-data, powering vault_update_task's write-format
detection) — so both categories default on; the desktop's per-device
Vault configuration sync toggles remain the real gate. #428 blocks all
tool reads of .obsidian/, and the deploy docs note plainly that plugin
settings can carry API keys and sit unread on the config volume.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@umm-actually

umm-actually Bot commented Aug 13, 2026

Copy link
Copy Markdown

Document the memory dot-file message that memory tools actually emit
Low severity · correctness · high confidence

PR description:40 — beyond the diff's line ranges, in code the changes touch or depend on.

The PR's behavior-change list states that vault_get_memory / vault_update_memory with a dot-prefixed file name return hidden path blocked: ... targets a hidden file or folder. The implementation (and the memory tool descriptions changed in the same PR) throw memory file must not start with a dot: ... would be a hidden file instead, so the release notes advertise a message the memory layer never produces.

Failure scenario: An operator upgrading and then verifying the fix greps tool logs for the documented string hidden path blocked; memory-tool rejections never emit it (they throw memory file must not start with a dot), so the documented verification step finds nothing and the operator cannot confirm the memory surface is blocked via the advertised message.

Suggested fix
In the Behavior changes section, split the memory tools out of the `hidden path blocked` bullet and document their actual message: `vault_update_memory` / `vault_get_memory` with a dot-prefixed `file` name return `memory file must not start with a dot: "..." would be a hidden file`.

aliasunder added a commit that referenced this pull request Aug 13, 2026
… default-on SYNC_CONFIGS (#429)

Closes #427.

Stacked on #428 (`feat(security): block hidden paths in all vault
operations`), now merged — this PR targets `main` and its diff shows
only the daily-notes work.

## What this fixes

Three gaps from #427:

1. **Daily-notes settings were unreachable on standard remote
deployments.** `SYNC_CONFIGS` was supported by the init chain but passed
through by no shipped compose file and documented nowhere, so
`.obsidian/daily-notes.json` never reached the server — daily notes
worked only when the vault's settings happened to match the defaults.
2. **A failed config read was cached for the process lifetime.** On a
fresh remote deploy the server boots before the initial sync delivers
`.obsidian/`, and `server.ts` reads the config at startup — so the boot
race was guaranteed, and the defaults stayed locked in until a restart.
3. **Periodic Notes users had no way to point the server at the right
folder** — the core `daily-notes.json` file (which vault-cortex reads)
isn't updated by the Periodic Notes plugin.

## Changes

- **`DAILY_NOTES_FOLDER` / `DAILY_NOTES_FORMAT` env settings** with
per-field precedence: env setting → `.obsidian/daily-notes.json` → the
fallbacks (`Daily Notes`, `YYYY-MM-DD`). The format keeps Obsidian's
moment tokens; validation at startup is structural fail-fast
(probe-render a fixed date; reject traversal, leading or trailing
separators, and empty renders) — unknown tokens are not rejected —
tokens without a Luxon mapping fall into Luxon's own token grammar and
may render differently than Obsidian, so only structurally unsafe
results are blocked. When both fields are overridden, the config file is
not read at all. The env settings are threaded as params
(`DailyNotesEnvSettings`) from `VaultConfig` through
`vault_get_daily_note`, the daily-review prompt, and the startup read —
no module-level setter.
- **Config caches hold only successful reads.** `daily-notes.ts` and
`task-format-config.ts` (same race class) no longer cache the
missing/malformed-file fallback — it's recomputed per call, so a config
file delivered by sync after boot is picked up without a restart. Cost
while the file is absent: one failed `readFile` per call. Once a read
succeeds it's cached for the process lifetime, as before. The
broken-link forward-reference exclusion no longer relies on a
startup-seeded copy: `getOutgoingLinks` and `brokenLinkCount` take the
daily notes folder as a parameter, resolved fresh per call by their
callers, so a config file that arrives after boot corrects the
annotation and count without a restart.
- **`SYNC_CONFIGS` defaults to
`core-plugin-data,community-plugin-data`** — the two categories the
server reads: daily-notes settings, and community plugin settings (the
Tasks plugin's format, which `vault_update_task`'s write-format
detection uses). Applied in the remote compose files, the init script
(so plain `docker run`, where compose interpolation doesn't apply,
behaves the same), and CI deploys (optional repo-variable passthrough,
`PROTECTED_PATHS`-style). Pulling a category is a no-op unless the
desktop pushes it via Settings → Sync → "Vault configuration sync" —
those per-device toggles are the real gate. Disabling is the explicit
`none` sentinel, which actively clears the category list (`ob
sync-config --configs ""`) rather than skipping — on a persistent config
volume, skipping would leave a previously enabled category on across
boots. Community plugin settings can carry API keys for plugins that use
external services; #428 blocks all tool reads of `.obsidian/` and the
server opens only the two config files it needs, so synced settings
otherwise sit unread in the config volume (noted in the deploy docs).
- **`moment-format.ts` extracted** from `daily-notes.ts` as a pure
zero-import module so `config.ts` can validate formats without pulling
in filesystem/logger deps. Token conversion runs only outside [literal]
escapes, so literal text containing token letters ("[Week A]") is
preserved verbatim — the previous convert-after-quoting order corrupted
it (pre-existing, surfaced by review).
- **Docs**: README Configuration rows + a "Daily notes" section
(precedence, remote sync mechanics, Periodic Notes guidance);
deploy/remote README "Daily notes" section + config table rows;
DEPLOY.md CI variables; ARCHITECTURE.md resolution chain; `.env.example`
× 3; synced CLI env blocks.
- **DOCKERHUB generator**: the new table rows pushed the generated
README past Docker Hub's 25000-byte cap (it was at 24785), so the
generator now collapses markdown table padding (renders identically) and
drops H3 subsections inside compact sections instead of emitting an
empty heading. Output is now 18873 bytes.

## Behavior change

Remote deployments that previously left `SYNC_CONFIGS` unset now sync
the `core-plugin-data` and `community-plugin-data` categories by
default. This only has an effect when the desktop pushes those
categories; set `SYNC_CONFIGS=none` to keep config sync fully off.

## Tests

- Precedence matrix (folder-only / format-only /
both-vs-conflicting-file / both-no-file / folder-only-no-file),
ENOENT-retry and malformed-then-fixed for both config readers,
success-cache behavior pinned, `getDailyNotePath` exact path with
overrides.
- `loadConfig`: defaults, empty/whitespace-as-unset, raw moment string
preserved, nested `YYYY/MM/DD` accepted, exact failure messages for
traversal (raw and rendered), leading and trailing separators (raw and
rendered), and empty render.
- Exact-form drift pin: `SYNC_CONFIGS:
${SYNC_CONFIGS:-core-plugin-data,community-plugin-data}` in both remote
compose files; existing compose ↔ .env.example ↔ CLI-block consistency
tests cover the new vars.
- Mutation-verified: reverting the cache fix makes exactly the three
retry tests fail; all four `SYNC_CONFIGS` states (unset / empty / `none`
/ custom) exercised against the init script's branch logic with stubbed
commands under `sh -e`.
- `npm run lint` clean, both builds green, full suite 2527 tests
passing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added configurable daily-note folders and filename formats, with
environment overrides and vault/Obsidian fallbacks.
* Added Moment-to-Luxon date-format compatibility, including literal
text handling.
* Added configurable Obsidian settings synchronization, defaulting to
core plugin data.

* **Bug Fixes**
* Configuration files now retry after missing or malformed reads while
retaining valid cached settings.
* Added validation to prevent unsafe or invalid daily-note paths and
filenames.

* **Documentation**
  * Expanded deployment, configuration, and daily-notes documentation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant