Skip to content

fix(security): contain Template/Capture file writes and package reads within the vault - #1460

Merged
chhoumann merged 4 commits into
masterfrom
chhoumann/sec-template-path-traversal
Jun 30, 2026
Merged

fix(security): contain Template/Capture file writes and package reads within the vault#1460
chhoumann merged 4 commits into
masterfrom
chhoumann/sec-template-path-traversal

Conversation

@chhoumann

@chhoumann chhoumann commented Jun 30, 2026

Copy link
Copy Markdown
Owner

Summary

Four deepsec MEDIUM path-traversal findings share one root cause: a formatted file path could
escape the vault because normalizeGeneratedFilePath blocked forward-slash .. but not
backslash traversal (..\..\..\evil) or absolute paths, and the create / package-read sinks had
no vault-containment assertion. All four are fixed at the root and proven RED→GREEN in a live
Obsidian vault (macOS, Obsidian 1.13.0).

QuickAdd runs with full user trust, so these are only real because the path is attacker-influenceable:
an obsidian://quickadd URI is reachable from any webpage; the CLI and synced data.json / note
content carry untrusted strings.

The vulnerabilities (all reproduced live)

#1 + #2 — out-of-vault WRITE (Template/Capture create sink).
obsidian://quickadd?choice=<T>&value-value=..\..\..\evil seeds the value variable
(applyUriValueParameters blocks only __qa. keys). For a Template whose fileNameFormat is
{{value}}, it flows verbatim through normalizeGeneratedFilePath (which split only on /, so the
backslash payload survived as one non-.. segment) into vault.create. Obsidian normalizes \/
before writing, so the path resolved outside the vault.

RED (live): a malicious URI created a folder QA_PWN_DIR and a note evil.md OUTSIDE the
vault root, with the formatted template body as content.
GREEN: rejected (File name cannot contain "." or ".." path segments); nothing created outside.

#3 — discovery-derived titles. The same gap let a planted [[..\..\evil]] unresolved link
(sourced from untrusted note content) surface as a "create" candidate. Covered by the root fix;
additionally filtered so it never appears as a lure.

#4 — out-of-vault READ (CLI/package read entry). quickadd:package-preview path=../../../etc/passwd
reached adapter.read after only normalizePath (which does not resolve ..).

RED (live): quickadd:package-preview path=../../../../etc/hosts leaked /etc/hosts content via
the parse-error envelope.
GREEN: Refusing to read a package outside the vault.

The fix (two layers, mirroring the package-import containment in #1434)

  • Layer A — root normalization (generatedFilePath.ts): convert \/ before the segment
    split, so backslash traversal is rejected exactly like its forward-slash form. This also makes the
    normalizer's view of the path identical to what Obsidian's own normalizePath writes to disk
    (latent inconsistency fixed).
  • Layer B — authoritative sink guard (QuickAddEngine.createFileWithInput): the single create
    sink shared by the Template and Capture engines throws escapesVaultBoundary(filePath) before
    any filesystem touch (before folder pre-creation and vault.create), so absolute / drive / UNC /
    .. paths can never escape even if an upstream normalizer is bypassed.
  • Read entry (readQuickAddPackage): throws escapesVaultBoundary(packagePath) before
    adapter.exists/adapter.read, centralizing the guard the sibling analysePackage /
    analysePackagePreview probes already have (fix(security): contain untrusted package-import paths to the vault #1434) so every caller (CLI + GUI import modal) inherits it.
  • Discovery filter (normalizeUnresolvedTarget): drops unresolved-link candidates that escape
    the boundary.

Adversarial review → two more sibling sinks contained

An adversarial panel (3 lenses: bypass-hunter, sink-completeness, regression) was run against the
core fix. It confirmed the core fix is sound (the \/ change verified behavior-preserving
against extracted Obsidian 1.13.0 internals; encoding/Unicode/drive bypass classes all
neutralized), and surfaced two sibling sinks of the same vulnerability class that the create-sink
guard missed:

  • Apply-template note relocation (MEDIUM, confirmed). maybeReconcileNoteLocation
    (applyTemplateToActiveNote.ts) builds the move target via computeChoiceTargetPath
    normalizeTemplateFilePath, whose folder portion is only stripLeadingSlash'd (never run
    through normalizeGeneratedFilePath, not gated by validateFolderPath). A synced/shared Template
    choice with folder="../../../evil" assembled an out-of-vault target that flowed into
    vault.createFolder + fileManager.renameFile, moving the active note OUT of the vault.

    RED (live): fileManager.renameFile(f, "../../../X/moved.md") moved the note to
    …/quickadd/X/moved.md, outside the vault. Fix: assert escapesVaultBoundary on the assembled
    path at the shared normalizeTemplateFilePath chokepoint (covers create + relocation). GREEN
    (unit + live create flow): escaping folders throw; legit creates/relocations unaffected.

  • Userscript-path read on duplicate-choice (LOW). choiceService.buildSecretOptionNamesByPath
    read userscript paths from a Macro choice's data.json via adapter.read with no guard. Skip
    out-of-vault paths, mirroring the package probes.

Plus a hardening of the shared boundary helper: Windows trailing-dot/space .. collapse. Win32
strips trailing spaces/dots per component, so .. / ... resolve to .. at file-open;
escapesVaultBoundary's === ".." (and Node's own path math) missed them. Now any segment that is
.. followed only by spaces/dots is treated as traversal — load-bearing so the relocation guard is
robust on Windows, and strengthening every caller (create sink + package guards).

Scope decisions

  • escapesVaultBoundary (lexical), not assertWriteStaysInVault (realpath), at the create sink.
    The findings are about lexical traversal (..\, absolute). escapesVaultBoundary is the right tool:
    pure, no false-positives, and it deliberately allows in-vault dot-dirs — whereas the realpath writer
    guard also rejects writes that resolve into a config dir, which would break legit Template/Capture
    creation into a literal dot-folder. Symlink-mediated escape is a separate vector not in these findings.
  • Layer A only converts \/; it does not reject absolute/drive paths. Rejecting a leading /
    there would break the documented root-relative /Folder/Note convenience (asserted by an existing
    test). Absolute/drive/UNC containment lives in one place: the sink (Layer B).
  • Audited adjacent sinks — already contained, no change needed: the folder-selection
    createFolder (validateFolderSegment rejects \ / : .. … per segment — verified live: a backslash
    folder collapses to vault root); applyTemplateToActiveNote (path via computeChoiceTargetPath
    Layer A, plus an interactive confirm); the import asset writer (validateAssetDestination /
    assertWriteStaysInVault, fix(security): contain untrusted package-import paths to the vault #1434).

Tests (red-before / green-after, unit)

  • generatedFilePath.test.ts: backslash traversal rejected; backslash treated as a separator.
  • QuickAddEngine.test.ts: the create sink refuses 7 escaping forms (backslash, POSIX, mid-path,
    absolute, drive ×2, UNC) and never touches the FS; still creates ordinary in-vault and in-vault
    dot-dir paths.
  • packageImportService.test.ts: the read entry refuses 8 escaping forms with no FS touch.
  • templateNoteDiscovery.test.ts: escaping unresolved-link candidates dropped; legit titles pass.
  • TemplateEngine.vault-containment.test.ts: the shared normalizeTemplateFilePath chokepoint
    refuses escaping folder portions (the relocation gap) and the Windows trailing-space ..; legit
    and leading-slash root-relative folders assemble fine.
  • vaultPathBoundary.test.ts: Windows trailing-dot/space .. collapse rejected; ordinary
    dotted names not over-rejected.
  • choiceService.test.ts: an out-of-vault userscript path is never stat/read on duplicate-choice.

Verification

tsc -noEmit ✅ · eslint . ✅ · svelte-check ✅ (0 errors) · vitest ✅ (3481 passed) ·
live RED→GREEN for the URI write, the CLI read, and the relocation rename sink, plus a positive
control (value-value=LegitNotes/Hello World still creates the in-vault note).

Summary by CodeRabbit

  • Bug Fixes
    • Blocked file creation, template generation, capture targets, and package reads from accepting paths that escape the vault, including traversal, absolute, drive, and UNC-style forms.
    • Prevented malicious unresolved-link targets and template/note “create” options from surfacing when they would resolve outside the vault.
    • Improved handling of Windows-style backslashes in generated paths to ensure consistent validation across platforms.
    • Skipped unsafe out-of-vault synced/untrusted user-script entries during choice duplication to avoid unintended filesystem access.

A formatted file name that reached the create sink could escape the vault.
`normalizeGeneratedFilePath` split only on '/', so a backslash-traversal name
like `..\..\..\evil` survived its `.`/`..` rejection as a single segment, then
`vault.create` resolved it (Obsidian normalizes '\'->'/' before writing) and
wrote OUTSIDE the vault root. The name is attacker-influenceable: an
`obsidian://quickadd?...&value-value=..\..\..\evil` URI (reachable from any
webpage) seeds the `value` variable, and for a Template choice whose
fileNameFormat is `{{value}}` it flows verbatim into the create path. Proven
end-to-end in a live vault: a malicious URI created a folder and note OUTSIDE
the vault with the formatted template body.

Two-layer fix, mirroring the package-import containment (#1434):

- normalizeGeneratedFilePath now converts '\'->'/' before the segment check, so
  backslash traversal is rejected like its forward-slash form and the
  normalizer's view of the path matches what Obsidian actually writes.
- createFileWithInput (the single create sink shared by the Template and Capture
  engines) asserts escapesVaultBoundary BEFORE any filesystem touch, so an
  absolute / drive / UNC / `..` path can never escape even if an upstream
  normalizer is bypassed.

Also drop unresolved-link discovery candidates that escape the vault boundary,
so a planted `[[..\..\evil]]` in synced note content never surfaces as a
selectable create target.
previewPackageHandler (the `quickadd:package-preview` CLI) passes an untrusted
`path=` flag straight to readQuickAddPackage, which called
`adapter.exists`/`adapter.read` after only `normalizePath` - which collapses
slashes but does NOT resolve `..`. So `path=../../../etc/passwd` read a file
OUTSIDE the vault: proven live, the CLI leaked /etc/hosts content via the
parse-error envelope. The sibling analysePackage/analysePackagePreview probes
already guard with escapesVaultBoundary (#1434); the one read entry point did
not.

Add the same escapesVaultBoundary guard at the top of readQuickAddPackage,
before any filesystem touch, so every current and future caller (CLI and the
GUI import modal) inherits the protection and an absolute / drive / `..` path is
refused up front.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 30, 2026

Copy link
Copy Markdown

Deploying quickadd with  Cloudflare Pages  Cloudflare Pages

Latest commit: 68751dc
Status: ✅  Deploy successful!
Preview URL: https://83e56ec4.quickadd.pages.dev
Branch Preview URL: https://chhoumann-sec-template-path.quickadd.pages.dev

View logs

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 55aaee29-b1ea-48bc-954f-4b810174a446

📥 Commits

Reviewing files that changed from the base of the PR and between 5095c6f and 68751dc.

📒 Files selected for processing (8)
  • src/engine/CaptureChoiceEngine.selection.test.ts
  • src/engine/CaptureChoiceEngine.ts
  • src/engine/TemplateEngine.ts
  • src/engine/TemplateEngine.vault-containment.test.ts
  • src/services/choiceService.test.ts
  • src/services/choiceService.ts
  • src/utils/vaultPathBoundary.test.ts
  • src/utils/vaultPathBoundary.ts
✅ Files skipped from review due to trivial changes (1)
  • src/utils/vaultPathBoundary.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/engine/TemplateEngine.ts
  • src/engine/TemplateEngine.vault-containment.test.ts
  • src/services/choiceService.ts
  • src/services/choiceService.test.ts
  • src/utils/vaultPathBoundary.ts

📝 Walkthrough

Walkthrough

Adds vault-boundary checks in engine and service path normalization, hardens traversal detection for Windows-style forms, and expands tests around rejection of out-of-vault paths and acceptance of valid in-vault paths.

Changes

Vault Path Traversal Containment

Layer / File(s) Summary
vaultPathBoundary and generatedFilePath utility hardening
src/utils/vaultPathBoundary.ts, src/utils/vaultPathBoundary.test.ts, src/utils/generatedFilePath.ts, src/utils/generatedFilePath.test.ts
hasTraversalSegment now treats .. variants with trailing dots/spaces as traversal, and normalizeGeneratedFilePath converts backslashes to forward slashes before segment splitting. Tests cover the new traversal forms and backslash normalization.
QuickAddEngine and TemplateEngine containment guards
src/engine/QuickAddEngine.ts, src/engine/QuickAddEngine.test.ts, src/engine/TemplateEngine.ts, src/engine/TemplateEngine.vault-containment.test.ts, src/engine/CaptureChoiceEngine.ts, src/engine/CaptureChoiceEngine.selection.test.ts
createFileWithInput, normalizeTemplateFilePath, and normalizeCaptureFilePath now reject computed paths outside the vault before filesystem or capture operations. New tests assert refusal for traversal, absolute, drive, and UNC inputs and confirm normal in-vault paths still pass.
templateNoteDiscovery, choiceService, and packageImportService guards
src/engine/templateNoteDiscovery.ts, src/engine/templateNoteDiscovery.test.ts, src/services/choiceService.ts, src/services/choiceService.test.ts, src/services/packageImportService.ts, src/services/packageImportService.test.ts
normalizeUnresolvedTarget returns null for escaping unresolved targets; buildSecretOptionNamesByPath skips vault reads for escaping user-script paths; readQuickAddPackage throws before adapter access for escaping package paths. Tests cover the early exits and skipped vault calls.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • chhoumann/quickadd#1390: Also modifies TemplateEngine.normalizeTemplateFilePath path handling and validation in the same area.
  • chhoumann/quickadd#1434: Adds escapesVaultBoundary-based guards to packageImportService to prevent out-of-vault reads.
  • chhoumann/quickadd#1304: Uses the same early-rejection pattern for unsafe paths before adapter access.

Poem

🐇 I hopped through the vault with a careful sniff,
No .. in the burrow, no sneaky path drift.
Backslashes, dots, and drive letters too,
Got bounced at the gate with a thorough “nope, shoo!”
My carrot stash stays safe in its place,
With tidy paths and a secure little space.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main security change: containing Template/Capture writes and package reads within the vault.
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.
✨ 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 chhoumann/sec-template-path-traversal

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.

…inks

An adversarial review of the create-sink fix surfaced two sibling sinks that
reach the filesystem with the SAME attacker-influenceable, uncontained path and
were missed by the create-sink guard:

- Apply-template note relocation. `maybeReconcileNoteLocation`
  (applyTemplateToActiveNote.ts) builds the move target via
  `computeChoiceTargetPath` -> `normalizeTemplateFilePath`, whose FOLDER portion is
  only `stripLeadingSlash`'d (never run through `normalizeGeneratedFilePath` and
  not gated by `validateFolderPath`). A synced/shared Template choice whose folder
  is "../../../evil" assembled an out-of-vault target that flowed into
  `vault.createFolder` + `fileManager.renameFile`, moving the active note OUTSIDE
  the vault (the rename/createFolder escape is live-proven, same adapter as the
  create-sink escape). Fix: assert `escapesVaultBoundary` on the assembled path at
  the shared `normalizeTemplateFilePath` chokepoint, so both the create flow and
  the relocation flow are contained; `computeChoiceTargetPath` then throws before
  the move prompt and no rename happens.

- Userscript-path read on duplicate-choice. `choiceService.buildSecretOptionNamesByPath`
  read userscript paths from a Macro choice's `data.json` config via `adapter.read`
  with no boundary guard, so a synced/imported Macro command path of
  "../../../etc/passwd" was stat+read out of the vault. Skip out-of-vault paths,
  mirroring the package-import probes.

Also harden `escapesVaultBoundary` against the Windows trailing-dot/space "..":
the Win32 filesystem strips trailing spaces/dots per component, so ".. " / "..."
resolve to the parent ".." at file-open. The lexical `=== ".."` check (and Node's
own path math) missed those, so a folder like ".. /.. /evil" could survive on the
relocation path (which bypasses Layer A). Match any segment that is ".." followed
only by spaces/dots. This strengthens every escapesVaultBoundary caller, including
the create sink and the package guards.
@chhoumann
chhoumann force-pushed the chhoumann/sec-template-path-traversal branch from 5095c6f to 12c93ff Compare June 30, 2026 08:05

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5095c6f601

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/engine/QuickAddEngine.ts

@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: 1

🤖 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/engine/TemplateEngine.vault-containment.test.ts`:
- Around line 94-98: The regression test in
TemplateEngine.vault-containment.test.ts is failing too early because
normalizeGeneratedFilePath rejects the input before the assembled-path guard is
exercised. Update the test around engine.normalize so the crafted folder/name
input survives name normalization and reaches
escapesVaultBoundary(assembledPath), then assert the thrown error contains the
“outside the vault” message. Keep the test focused on the assembled-path
defense-in-depth behavior described in the existing it("refuses traversal that
appears only in the assembled file name") case.
🪄 Autofix (Beta)

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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3eb98fd3-148a-486f-90f0-ccf2ed5811db

📥 Commits

Reviewing files that changed from the base of the PR and between 9c9655e and 5095c6f.

📒 Files selected for processing (14)
  • src/engine/QuickAddEngine.test.ts
  • src/engine/QuickAddEngine.ts
  • src/engine/TemplateEngine.ts
  • src/engine/TemplateEngine.vault-containment.test.ts
  • src/engine/templateNoteDiscovery.test.ts
  • src/engine/templateNoteDiscovery.ts
  • src/services/choiceService.test.ts
  • src/services/choiceService.ts
  • src/services/packageImportService.test.ts
  • src/services/packageImportService.ts
  • src/utils/generatedFilePath.test.ts
  • src/utils/generatedFilePath.ts
  • src/utils/vaultPathBoundary.test.ts
  • src/utils/vaultPathBoundary.ts

Comment thread src/engine/TemplateEngine.vault-containment.test.ts Outdated
…sink

Review follow-up (PR #1460). `CaptureChoiceEngine.run` calls `fileExists(filePath)`
BEFORE `createFileWithInput`, and `normalizeGeneratedFilePath` intentionally leaves
absolute/drive/UNC paths for the boundary check — so a 'Capture to' formatting to a
Windows drive path like "C:/secret.md" reached `adapter.exists` out-of-vault before
the create-sink guard could reject it (the first filesystem touch was uncontained).

Assert `escapesVaultBoundary` on the assembled path inside `normalizeCaptureFilePath`,
mirroring the Template path's `normalizeTemplateFilePath` guard, so the escape is
rejected at assembly — before the existence probe. Proven live: a "C:/secret.md"
Capture target now aborts with "Refusing to capture to a file outside the vault" and
`adapter.exists` is never called with the drive path; legit in-vault captures still work.

Also tighten the assembled-path regression test to exercise the
`escapesVaultBoundary(assembledPath)` guard specifically (a drive path that survives
name normalization) instead of an input that throws earlier in name normalization.
@chhoumann
chhoumann merged commit b4cb0f6 into master Jun 30, 2026
11 checks passed
@chhoumann
chhoumann deleted the chhoumann/sec-template-path-traversal branch June 30, 2026 19:20
quickadd-release-bot Bot pushed a commit that referenced this pull request Jul 2, 2026
# [2.14.0](2.13.1...2.14.0) (2026-07-02)

### Bug Fixes

* **ai-tools:** tool-confirm safety, agent progress feedback, and step accounting ([#1414](#1414)) ([76ff8e4](76ff8e4))
* **ai:** close allowedRoots fence bypass + honest truncated flag in vault read tools ([#1443](#1443)) ([9d718b1](9d718b1)), closes [#1432](#1432)
* **ai:** enforce allowedRoots confinement in workspace tools ([#1432](#1432)) ([52c32c8](52c32c8))
* **ai:** keep OpenAI assistant content as a string ([9a8656c](9a8656c))
* **ai:** provider modals, model discovery errors, and provider-neutral messaging ([#1413](#1413)) ([b0f2c26](b0f2c26))
* append links to focused properties ([d83ed68](d83ed68))
* build folder-capture path without a double slash ([#1307](#1307)) ([a7caab0](a7caab0))
* **capture,template:** insertion targets, canvas/base guard, links, and feedback ([#1412](#1412)) ([bc2c765](bc2c765))
* **capture:** abort inline insert-after on a multi-line target instead of silently missing ([#468](#468)) ([#1370](#1370)) ([076d54b](076d54b)), closes [#1333](#1333)
* **capture:** allow clean concurrent-edit merges ([#1302](#1302)) ([a3774d6](a3774d6))
* **capture:** allow empty scoped target creation ([f0a72e6](f0a72e6))
* **capture:** clarify scoped target selection ([b620325](b620325))
* **capture:** find multi-line insert-after/before targets instead of duplicating them ([#1333](#1333)) ([eaa489d](eaa489d)), closes [#742](#742)
* **capture:** keep cursor after opened captures ([#1392](#1392)) ([201e084](201e084))
* **capture:** preserve literal backslash sequences in captured content ([#1301](#1301)) ([02d77a2](02d77a2)), closes [#527](#527) [#527](#527) [#527](#527)
* **capture:** scope ordered-search fence masking to the note body ([8107456](8107456)), closes [#1404](#1404)
* **capture:** stop adding a blank line after a task-formatted insert-after capture ([#1355](#1355)) ([5094b91](5094b91)), closes [#312](#312) [#312](#312) [#312](#312)
* **choice-list:** preserve folder children when editing from a filtered view ([#1353](#1353)) ([9407060](9407060))
* **choices:** assign unique default names for new choices ([#1318](#1318)) ([#1319](#1319)) ([5e08337](5e08337))
* **cli:** abort non-interactive runs instead of hanging on engine prompts ([#1428](#1428)) ([c529b22](c529b22))
* **core:** command registration, multi-folder UX, URI/CLI guards, and update announcements ([#1410](#1410)) ([cc075ea](cc075ea))
* **core:** resolve 7 deepsec correctness/robustness BUG findings (core data utilities) ([#1437](#1437)) ([6a27141](6a27141))
* deepsec core-src BUG cluster (FILE-token collision, proto-key lookups, folder guard, null-deref) ([#1442](#1442)) ([0846fba](0846fba))
* default frontmatter append handling to create or convert ([fedb0dc](fedb0dc))
* **deps:** bump undici to 6.27.0 ([a4ff987](a4ff987))
* **deps:** patch vulnerable undici and js-yaml ([3e38189](3e38189))
* **example-scripts:** three deepsec robustness/data-integrity fixes ([#1465](#1465)) ([b7fcb04](b7fcb04)), closes [#1431](#1431) [#1454](#1454)
* **field:** remove two ReDoS regexes from inline-field parsing ([#1444](#1444)) ([2e76914](2e76914)), closes [#1439](#1439) [#1128](#1128) [#1439](#1439)
* **field:** wikilink-aware inline-field parsing + stop migration deleting code blocks ([#1439](#1439)) ([87e71a0](87e71a0))
* **file-suggester:** register one document scroll listener per instance instead of per row ([#1309](#1309)) ([217fce1](217fce1))
* **format:** handle non-string trimmed values ([1d6de9f](1d6de9f))
* **format:** resolve current-file/note tokens in one pass to stop rescan loops ([#1358](#1358)) ([#1369](#1369)) ([7b61d91](7b61d91))
* **formatter:** consume empty tokens without looping ([#1303](#1303)) ([0ce7951](0ce7951))
* **format:** token previews, value-syntax warnings, and field-token parsing ([#1411](#1411)) ([0a519ef](0a519ef))
* frontmatter-aware top insertion for capture and apply-template ([#1334](#1334)) ([0af26f5](0af26f5)), closes [#647](#647)
* guard template expansion against inclusion cycles ([#1308](#1308)) ([e705d75](e705d75))
* **gui:** preserve choice-builder edit position by converting the ChoiceBuilder trio to Svelte ([#1130](#1130)) ([#1330](#1330)) ([2681dc3](2681dc3))
* **gui:** stop DataCloneError when configuring a choice nested in a folder or Macro ([#1350](#1350)) ([7a7c780](7a7c780))
* **integrations:** field-value suggestion filters and defaults ([#1417](#1417)) ([6e36797](6e36797))
* **macro:** correct conditional abort, deleted-choice handling, and command validation ([#1409](#1409)) ([85eb77c](85eb77c))
* **macro:** harden user-script member access conflicts ([#964](#964)) ([#1326](#1326)) ([b0974df](b0974df))
* make the update modal closable on iPhone ([#635](#635)) ([#1327](#1327)) ([2b93029](2b93029))
* normalize generated note paths ([1abfb03](1abfb03))
* normalize generated path routing ([6c0f428](6c0f428))
* **package-import:** classify bundled scripts as critical ([#1305](#1305)) ([477957f](477957f))
* **preflight:** drain template includes in formatted paths ([5f33c2d](5f33c2d))
* **preflight:** ignore global-injected template includes ([c6c8915](c6c8915))
* **preflight:** match template include stack semantics ([4f377aa](4f377aa))
* **preflight:** mirror template include depth ([bb073a3](bb073a3))
* **preflight:** scan capture template includes ([42dc4aa](42dc4aa))
* **preflight:** skip multi capture filters as tag targets ([19b52f8](19b52f8))
* preserve frontmatter property key casing ([c8e555b](c8e555b))
* preserve property focus through choice suggesters ([db6817d](db6817d))
* **prompts:** suggester crashes, input handling, preflight, and API robustness ([#1415](#1415)) ([5f71ca9](5f71ca9))
* reject empty generated path segments ([f41e7ff](f41e7ff))
* render list front matter as real properties out of the box ([#662](#662)) ([#1338](#1338)) ([725b284](725b284))
* resolve Obsidian plugin review lint findings ([#1426](#1426)) ([72d13da](72d13da))
* resolve Obsidian reviewer code-quality findings ([#1320](#1320)) ([f9ddbfe](f9ddbfe))
* **security:** align capture-target scope classifier with the write-path resolver + case-insensitive assignToVariable guard ([#1458](#1458)) ([318b606](318b606)), closes [#1448](#1448) [#1448](#1448)
* **security:** close package-import disclosure gap and asset-path collisions ([#1445](#1445)) ([755e0fd](755e0fd))
* **security:** contain attacker-injectable capture-target redirection ([#1448](#1448)) ([f2b0c67](f2b0c67))
* **security:** contain Template/Capture file writes and package reads within the vault ([#1460](#1460)) ([b4cb0f6](b4cb0f6)), closes [#1434](#1434) [#1434](#1434)
* **security:** contain untrusted package-import paths to the vault ([#1434](#1434)) ([de54299](de54299))
* **security:** guard front matter assignment against prototype pollution ([#1431](#1431)) ([ce837be](ce837be))
* **security:** kill five quadratic-regex DoS hazards over untrusted content ([#1463](#1463)) ([f93a845](f93a845))
* **security:** kill quadratic ReDoS in VDATE token regex ([#1462](#1462)) ([aec6151](aec6151)), closes [#1455](#1455) [#1455](#1455)
* **security:** linearize two main-thread ReDoS regexes (caseTransform + FIELD_VAR_REGEX_WITH_FILTERS) ([#1455](#1455)) ([95826d3](95826d3))
* **security:** reject import packages with divergent same-id choices ([#1453](#1453)) ([9c9655e](9c9655e))
* **security:** reject unsafe package asset destinations ([#1304](#1304)) ([af20b5f](af20b5f))
* **security:** retry secret migration instead of stranding plaintext API keys ([#1457](#1457)) ([1cccddb](1cccddb))
* self-heal duplicate choice ids on load (blank settings page) ([#1461](#1461)) ([12ba5a4](12ba5a4)), closes [#each](https://github.com/chhoumann/quickadd/issues/each) [#1451](#1451) [#1451](#1451)
* **settings:** global-variable data loss, package overwrite guard, and settings UX ([#1416](#1416)) ([bd50f94](bd50f94))
* six deepsec correctness fixes (chunk gluing, capture gluing, preflight double-exec, template fan-out, proto lookups, O(H²) links) ([#1464](#1464)) ([5d31e84](5d31e84)), closes [#312](#312) [#1442](#1442)
* **suggester:** file-index reindex race + per-prompt tag-listener leak + suggester teardown ([#1446](#1446)) ([62adb09](62adb09)), closes [#-tag](https://github.com/chhoumann/quickadd/issues/-tag) [#-pre](https://github.com/chhoumann/quickadd/issues/-pre)
* **suggesters:** align Unicode highlight + remove two dead-code units ([#1456](#1456)) ([9beb266](9beb266))
* **template:** merge frontmatter on template append ([e14ff42](e14ff42))
* **template:** preserve append value prompts ([ff49fd8](ff49fd8))
* three deepsec data-integrity BUG fixes (collision non-termination, dropped imported child, prompt backslash corruption) ([#1459](#1459)) ([4547b22](4547b22)), closes [#799](#799)
* three deepsec functional bugs (folder scoping, migration guard, date preview) ([#1435](#1435)) ([3534cb1](3534cb1))
* **ui:** resolve deepsec UI/settings BUG findings (suggester leaks, AI modals, provider migration) ([#1438](#1438)) ([18d6186](18d6186)), closes [#1413](#1413) [pre-#1413](https://github.com/pre-/issues/1413) [#1413](#1413)

### Features

* add {{FILE:<folder>}} token to pick a file from a folder ([#1357](#1357)) ([851978e](851978e)), closes [#933](#933) [#1159](#1159)
* add frontmatter append link placement ([79d02ef](79d02ef))
* **ai:** LLM tool/function calling — ai.agent() + built-in tools ([#714](#714)) ([#1372](#1372)) ([3d97a16](3d97a16)), closes [Hi#risk](https://github.com/Hi/issues/risk)
* **append-link:** choose Link type independently of Link placement ([#1422](#1422)) ([f2f29f4](f2f29f4))
* **capture:** "Under heading…" write position with a runtime heading dropdown ([#1348](#1348)) ([246b8c5](246b8c5)), closes [#738](#738)
* **capture:** capture to an arbitrary frontmatter property ([#466](#466)) ([#1363](#1363)) ([507f9d4](507f9d4)), closes [#tag](https://github.com/chhoumann/quickadd/issues/tag)
* **capture:** copy captured note links to clipboard ([8495781](8495781))
* **capture:** order note picker by recency like Quick Switcher ([#745](#745), [#539](#539)) ([275b959](275b959))
* **capture:** ordered create-if-not-found location for reverse-chrono logs ([#481](#481)) ([#1365](#1365)) ([6838026](6838026))
* **capture:** support clipboard image attachments ([#1393](#1393)) ([4e7735b](4e7735b))
* **choice:** support icons in picker rows ([#1396](#1396)) ([3fa4330](3fa4330))
* **commands:** icon for choice commands on the mobile toolbar ([#766](#766)) ([#1342](#1342)) ([6e6f959](6e6f959))
* **field:** default {{FIELD}} from the active note's property (default-from:active) ([#1430](#1430)) ([22fde1f](22fde1f)), closes [#1429](#1429) [#1429](#1429) [#1429](#1429)
* **format:** add {{linksection}} token linking to the current heading ([#387](#387)) ([72901e9](72901e9)), closes [Note#Heading](https://github.com/Note/issues/Heading)
* **format:** add bounded slider value inputs ([#1387](#1387)) ([dc14d4a](dc14d4a))
* **format:** add trim option for value tokens ([2f2cefb](2f2cefb))
* **format:** date snap options |startof:/|endof: for {{DATE}} and {{VDATE}} ([#511](#511)) ([#1371](#1371)) ([d98904e](d98904e))
* **format:** named suggester reuse via {{VALUE:options|name:x}} ([#148](#148)) ([#1336](#1336)) ([3c10915](3c10915))
* **format:** support FIELD multi-select ([#1391](#1391)) ([0ad9c97](0ad9c97))
* **format:** support Obsidian property types in format syntax ([#757](#757)) ([#1367](#1367)) ([463ed3e](463ed3e)), closes [#1357](#1357) [#387](#387) [#621](#621) [Hi#severity](https://github.com/Hi/issues/severity) [#todo](https://github.com/chhoumann/quickadd/issues/todo)
* **formatters:** add {{FOLDER}} token for file names and paths ([#1328](#1328)) ([7db9033](7db9033)), closes [#1258](#1258)
* improve file and note suggesters ([4c98621](4c98621))
* **linking:** append file links to specified notes ([88e7d9b](88e7d9b))
* **macro:** run a user script from a note's ```js code block ([#1065](#1065)) ([#1341](#1341)) ([7428dff](7428dff))
* **prompt:** insert tab character in multi-line input prompt ([#764](#764)) ([f04aad2](f04aad2))
* **template:** add discovery-first note creation ([#1397](#1397)) ([da2f6aa](da2f6aa))
* **template:** copy created note link ([#1384](#1384)) ([5991a0e](5991a0e))
* **template:** replace folder-location toggles with a single dropdown ([#1131](#1131)) ([#1344](#1344)) ([3726cdb](3726cdb))
* **template:** run a template from the configured folder without a per-file choice ([#1023](#1023)) ([#1343](#1343)) ([ce2b08d](ce2b08d))
* **templates:** support format syntax in the template path ([#620](#620)) ([#1337](#1337)) ([c2ee449](c2ee449))
* **templates:** support multiple template folder paths ([#1170](#1170)) ([#1325](#1325)) ([0da7ef6](0da7ef6))
* **uri:** support x-callback-url for the QuickAdd URI ([#1070](#1070)) ([#1339](#1339)) ([f111d45](f111d45))
* **user-scripts:** store secret settings in SecretStorage ([#1373](#1373)) ([288a1b5](288a1b5))
* **user-scripts:** support stable secret option ids ([0b26d76](0b26d76))
* **value:** allow commas inside quoted multi-select options ([#239](#239)) ([#1345](#1345)) ([6e229b1](6e229b1))

### Performance Improvements

* **ai:** remove bundled tiktoken ([#1317](#1317)) ([72f1b91](72f1b91))
* **latex-suggester:** render math previews lazily ([#1310](#1310)) ([b35875e](b35875e))
* recompute FileIndex unresolved links lazily ([#1311](#1311)) ([ce17566](ce17566))
@quickadd-release-bot

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.14.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant