Vault Setup
earlyaidopters/second-brain
Interactive Obsidian vault configurator. An agent skill from earlyaidopters/second-brain.
Write declarative settings tabs for Obsidian plugins using the 1.13.0+ getSettingDefinitions() API.
$ npx skills add aidenlx/zotlit --skill obsidian-settings -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install aidenlx/zotlit obsidian-settings --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/aidenlx/zotlit.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.agents/skills/obsidian-settings .claude/skills/obsidian-settings && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "obsidian-settings" agent skill from https://github.com/aidenlx/zotlit/tree/main/.agents/skills/obsidian-settings into .claude/skills/obsidian-settings/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "obsidian-settings", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/aidenlx/zotlit/tree/main/.agents/skills/obsidian-settingsType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add aidenlx/zotlit --skill obsidian-settings -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install aidenlx/zotlit obsidian-settings --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/aidenlx/zotlit.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.agents/skills/obsidian-settings .agents/skills/obsidian-settings && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "obsidian-settings" agent skill from https://github.com/aidenlx/zotlit/tree/main/.agents/skills/obsidian-settings into .agents/skills/obsidian-settings/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "obsidian-settings", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add aidenlx/zotlit --skill obsidian-settings -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install aidenlx/zotlit obsidian-settings --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/aidenlx/zotlit.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.agents/skills/obsidian-settings .cursor/skills/obsidian-settings && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "obsidian-settings" agent skill from https://github.com/aidenlx/zotlit/tree/main/.agents/skills/obsidian-settings into .cursor/skills/obsidian-settings/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "obsidian-settings", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/aidenlx/zotlit.git --path .agents/skills/obsidian-settings--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add aidenlx/zotlit --skill obsidian-settings -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install aidenlx/zotlit obsidian-settings --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/aidenlx/zotlit.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.agents/skills/obsidian-settings .gemini/skills/obsidian-settings && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "obsidian-settings" agent skill from https://github.com/aidenlx/zotlit/tree/main/.agents/skills/obsidian-settings into .gemini/skills/obsidian-settings/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "obsidian-settings", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install aidenlx/zotlit obsidian-settingsInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add aidenlx/zotlit --skill obsidian-settings -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/aidenlx/zotlit.git skills-src && mkdir -p .github/skills && cp -r skills-src/.agents/skills/obsidian-settings .github/skills/obsidian-settings && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "obsidian-settings" agent skill from https://github.com/aidenlx/zotlit/tree/main/.agents/skills/obsidian-settings into .github/skills/obsidian-settings/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "obsidian-settings", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add aidenlx/zotlit --skill obsidian-settings -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install aidenlx/zotlit obsidian-settings --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/aidenlx/zotlit.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.agents/skills/obsidian-settings .opencode/skills/obsidian-settings && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "obsidian-settings" agent skill from https://github.com/aidenlx/zotlit/tree/main/.agents/skills/obsidian-settings into .opencode/skills/obsidian-settings/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "obsidian-settings", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
obsidian-settingsWrite declarative settings tabs for Obsidian plugins using the 1.13.0+ getSettingDefinitions() API.
Obsidian Settings is an agent skill from aidenlx/zotlit. Write declarative settings tabs for Obsidian plugins using the 1.13.0+ getSettingDefinitions() API. Use when creating or editing a PluginSettingTab, adding settings controls (toggle, text, number, dropdown, slider, file, folder, color), building settings groups/lists/sub-pages, wiring conditional visibility or validation, migrating from the imperative display() approach, or touching any file under apps/obsidian/src/services//setting-tab/. Also use when opening the settings modal from code, deep-linking to a…
Its SKILL.md is about 5.3k tokens, which your agent loads only when the skill is triggered. The skill folder holds 3 other files, including reference files (for example `references/migration-guide.md` and `references/settings-guide.md`).
It sits in Agent Workflows, covering Hooks and plugins. It works with Obsidian. The repository describes itself as: Bring your Zotero library into Obsidian. Create literature notes, insert citations, and annotate PDFs without leaving your vault. The licence is AGPL-3.0.
3 steps, taken from the first numbered list in SKILL.md.
Read from SKILL.md and the folder at commit 27f5752. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
rgFrom the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md.
From URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Obsidian Settings loads about 5.3k tokens when it runs, and up to ~18k if it reads all its reference files. Until then it costs about 203 tokens; SKILL.md has 1,823 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from aidenlx/zotlit at commit 27f5752, republished under its AGPL-3.0 licence (© aidenlx). 1,823 words, ~5,340 tokens.
.claude/skills/obsidian-settings/SKILL.md (or your agent's skills folder). This skill also uses 2 other files; get the full folder from GitHub.Settings tabs describe their UI as data — an array of definition objects returned from
getSettingDefinitions(). Obsidian handles rendering, search indexing, persistence, and validation.
The canonical source is packages/obsidian-api/obsidian.d.ts. Read the relevant types there when
you need precise signatures — the guide below covers usage patterns, not exhaustive type docs.
Quick lookup commands:
rg 'SettingControl<' packages/obsidian-api/obsidian.d.ts — all control typesrg 'SettingDefinition' packages/obsidian-api/obsidian.d.ts — all definition typesrg 'class PluginSettingTab' packages/obsidian-api/obsidian.d.ts — tab classrg 'class SettingPage' packages/obsidian-api/obsidian.d.ts — imperative sub-page classOverride getSettingDefinitions() on your PluginSettingTab subclass. Each entry in the returned
array is a SettingDefinitionItem — one of:
| Shape | What it does |
|---|---|
{ name, control: { type, key } } | Binds one settings key to a UI control. Auto-reads/writes/saves. |
{ name, render: (setting, group) => … } | Full imperative control over one Setting row. No auto-save. |
{ name, action: (el, index) => … } | Clickable action row. |
{ name } (no control/render/action) | Static heading or info row. |
{ type: 'group', heading, items } | Visual grouping with a heading. |
{ type: 'list', heading, items, onDelete, … } | User-managed collection (add/delete/reorder). |
{ type: 'page', name, items } or { type: 'page', name, page: () => … } | Navigable sub-page. |
control, render, and action are mutually exclusive on a single definition.
Every control reads from and writes to this.plugin.settings[key] automatically. Obsidian calls
saveData() after each change.
| Type | Stored value | Required fields | Optional fields |
|---|---|---|---|
toggle | boolean | key | defaultValue, disabled |
text | string | key | placeholder, defaultValue, validate, disabled |
textarea | string | key | placeholder, rows, defaultValue, validate, disabled |
number | number | key | min, max, step, placeholder, defaultValue, validate, disabled |
slider | number | key, min, max, step | defaultValue, displayFormat, disabled |
dropdown | string | key, options | defaultValue, disabled |
file | string (path) | key | filter, placeholder, defaultValue, validate, disabled |
folder | string (path) | key | filter, includeRoot, placeholder, defaultValue, validate, disabled |
color | string (hex) | key | defaultValue, disabled |
slider requires all three of min, max, step (not optional like on number).
// Toggle
{ name: 'Enable sync', control: { type: 'toggle', key: 'syncEnabled' } }
// Text with validation
{
name: 'API key',
control: {
type: 'text',
key: 'apiKey',
placeholder: 'Enter key…',
validate: (v) => v.length < 8 ? 'Must be at least 8 characters.' : undefined,
},
}
// Dropdown with default
{
name: 'Theme',
control: {
type: 'dropdown',
key: 'theme',
defaultValue: 'system',
options: { system: 'System', light: 'Light', dark: 'Dark' },
},
}
// Number with range
{
name: 'Max results',
control: { type: 'number', key: 'maxResults', min: 1, max: 100, defaultValue: 20 },
}
// Slider (min/max/step required)
{
name: 'Opacity',
control: { type: 'slider', key: 'opacity', min: 0, max: 100, step: 1 },
}
// Folder picker
{
name: 'Output folder',
control: { type: 'folder', key: 'outputDir', includeRoot: true },
}Two predicates toggle a setting's state without rebuilding the tab:
visible on any definition — hides the row when false. Hidden rows are excluded from search.disabled on a control or action — disables interaction without hiding.Both accept boolean | (() => boolean). The function form re-evaluates on every DOM-state refresh.
For control definitions, Obsidian refreshes automatically after every change.
getSettingDefinitions() {
return [
{ name: 'Advanced mode', control: { type: 'toggle', key: 'advanced' } },
{
name: 'Debug level',
visible: () => this.plugin.settings.advanced,
control: {
type: 'dropdown',
key: 'logLevel',
defaultValue: 'info',
options: { info: 'Info', verbose: 'Verbose' },
},
},
];
}Use visible when a setting is irrelevant in the current state. Use disabled when it exists but
is locked (prerequisite not met, feature not unlocked).
After mutating state from a render callback or other imperative path, call this.refreshDomState()
to re-run predicates without a full re-render. For changes that add/remove definitions (not just
toggle visibility), call this.update() instead.
Every control accepts an optional validate callback. Return a non-empty string to reject and show
an inline error. Return void/undefined/empty string to accept. Async validators work too.
{
name: 'Extension',
control: {
type: 'text',
key: 'ext',
validate: (v) => /\s/.test(v) ? 'No spaces allowed.' : undefined,
},
}validate is a UI gate, not a data invariant. Stored values may already be invalid (from older
plugin versions). Validate again when reading settings in loadSettings() if invariants matter.
Group related settings under a heading:
{
type: 'group',
heading: 'Appearance',
items: [
{ name: 'Font size', control: { type: 'number', key: 'fontSize', min: 8, max: 32 } },
{ name: 'Accent color', control: { type: 'color', key: 'accent' } },
],
}Groups also accept search (renders a filter input in the header — since 1.13.1), extraButtons,
cls, and visible.
Groups cannot nest inside groups. Use sub-pages for deeper hierarchy.
For user-managed collections (add/delete/reorder rows), use type: 'list':
{
type: 'list',
heading: 'Watched folders',
emptyState: 'No folders yet.',
addItem: {
name: 'Add folder',
action: () => this.openAddFolderModal(),
},
onReorder: async (oldIndex, newIndex) => {
let folders = this.plugin.settings.folders;
let [moved] = folders.splice(oldIndex, 1);
folders.splice(newIndex, 0, moved);
await this.plugin.saveData(this.plugin.settings);
},
onDelete: async (idx) => {
this.plugin.settings.folders.splice(idx, 1);
await this.plugin.saveData(this.plugin.settings);
this.update();
},
items: this.plugin.settings.folders.map((path) => ({
name: path,
searchable: false,
})),
}onReorder adds drag handles. DOM is already reordered — just update your data and save.onDelete wires both the delete button and the Delete key. Always call this.update() after.addItem renders a + button (desktop) / tappable row (mobile). Open a Modal for multi-field input.emptyState shows when items is empty.searchable: false on items keeps individual rows out of global search.For action rows inside lists, use action and read the live index argument (not the outer map index,
which goes stale after reorder):
items: commands.map((cmd) => ({
name: cmd.name,
searchable: false,
action: (el, index) => this.plugin.doSomething(index),
})),Navigable sub-pages for sections with self-contained scope. Use sparingly — only when the parent tab is too long to scan.
{
type: 'page',
name: 'Advanced',
desc: 'Power-user options.',
items: [
{ name: 'Debug logging', control: { type: 'toggle', key: 'debug' } },
{ type: 'group', heading: 'Cache', items: [
{ name: 'Size (MB)', control: { type: 'slider', key: 'cacheMb', min: 1, max: 500, step: 1 } },
]},
],
}Subclass SettingPage and pass a factory. Controls built in display() are invisible to
getSettingDefinitions() — no search indexing, no auto-save, no visible/disabled predicates.
import { SettingPage, Setting } from 'obsidian';
class StatusPage extends SettingPage {
constructor(private plugin: MyPlugin) {
super();
this.title = 'Status';
}
display() {
this.containerEl.empty();
new Setting(this.containerEl)
.setName('Refresh')
.addButton((btn) => btn.setButtonText('Refresh').onClick(() => this.display()));
}
hide() { /* clean up observers/timers */ }
}
// In getSettingDefinitions():
{ type: 'page', name: 'Status', page: () => new StatusPage(this.plugin) }items and page are mutually exclusive.
Page names must be unique among siblings at the same depth.
Since 1.13.1: displayValue shows a value on the page entry, status: 'warning' adds an indicator.
Use render when a setting needs side effects, derived values, or controls not covered by the
declarative types (moment format, progress bars, custom suggesters, multi-button rows).
{
name: 'Date format',
render: (setting) => {
setting.addMomentFormat((fmt) => fmt
.setValue(this.plugin.settings.dateFormat)
.onChange(async (v) => {
this.plugin.settings.dateFormat = v;
await this.plugin.saveData(this.plugin.settings);
}));
},
}render does not auto-save — always call saveData() yourself.
Return a cleanup function from render if you create subscriptions that outlive the DOM
(ResizeObserver, setInterval, etc.). Plain DOM listeners attached to elements inside the row are
cleaned up automatically.
By default, control definitions read/write this.plugin.settings[key] and auto-call saveData().
Override getControlValue(key) and setControlValue(key, value) to read/write a different store
(Svelte store, reactive proxy, immutable pattern). When overriding setControlValue, persist the
value yourself — the automatic saveData() call is replaced.
See references/settings-guide.md § "Custom settings storage" and § "Advanced: nested settings with
dot-notation keys" for the dot-path recipe.
If the tab displays state that changes outside the settings UI (vault contents, plugin computation),
call this.update() to rebuild from getSettingDefinitions(). Wire listeners in the constructor and
register through plugin.registerEvent(). Debounce bursty events.
constructor(app: App, plugin: MyPlugin) {
super(app, plugin);
this.plugin = plugin;
let refresh = debounce(() => this.update(), 200, true);
plugin.registerEvent(this.app.vault.on('create', refresh));
plugin.registerEvent(this.app.vault.on('delete', refresh));
}update() skips the row holding focusThe reconciler behind update()/refreshCurrentPage deliberately skips clearing and re-rendering
any setting row whose settingEl contains document.activeElement — it preserves focus while the
user edits a field. A matched row (stable key) only re-runs its render/control build when
e.setting && !e.settingEl.contains(activeElement).
Symptom: an action/button handler inside a row calls this.update(); every other row
refreshes but the clicked row stays stuck on its pre-action state, because the clicked button is
activeElement and lives in that row.
Fix: blur the button before triggering the update, e.g. btn.extraSettingsEl.blur() (or
el.blur()) before the async work. Modal-driven actions are unaffected — focus is on the modal, not
the row.
app.setting is the settings modal. It is internal API — declared in
apps/obsidian/src/typings/obsidian-ex.d.ts, verified against Obsidian 1.13. Reach for it when a
notice, ribbon action, or command sends the user to a specific place in settings.
There is no setting id. Definitions carry name, desc, aliases, and (for controls) a storage
key — search and navigation use none of them as an address. A sub-page is addressed by its page
name path, and a row is addressed by the definition object itself (identity, not id).
app.setting.open();
app.setting.openTabById(plugin.manifest.id);open() is idempotent and synchronous, so this is safe whether or not the modal already shows.
Which window it lands in depends on config — see Verifying on screen.
openTabById returns the SettingTab or null, and does not open the modal on its own. Built-in tab
ids in 1.13: about, appearance, editor, file, interface, hotkeys, keychain, plugins,
community-plugins. sync and publish are core-plugin tabs.
navigateToSearchResult is the one call that does tab → descend N sub-pages → reveal. It reads only
tab, pagePath, and result.entry.definition, so a plain object literal drives it — no real search
result needed. It short-circuits when the tab and page stack already match, so repeat calls are cheap.
function openSettingsPage(app: App, tabId: string, pagePath: string[]): boolean {
app.setting.open();
const tab = app.setting.openTabById(tabId);
if (!tab) return false;
app.setting.navigateToSearchResult({ tab, pagePath });
return true;
}
// Two levels deep, outermost page first.
openSettingsPage(app, plugin.manifest.id, [m.settingsPageZotero(), m.settingsPageDatabase()]);pagePath holds the exact name strings getSettingDefinitions() returned. Derive them from the
same message source as the definitions so they stay correct in every locale.
Two ways. Both scroll the row into view, focus it, and flash it for 750 ms — the same effect as clicking a settings search result.
By query — works for any tab, including built-in ones:
function revealSettingByQuery(app: App, tabId: string, query: string): boolean {
app.setting.open();
const group = app.setting.searchIndex
.search(query)
.find((g) => g.tab.id === tabId && g.results.length > 0);
if (!group) return false;
app.setting.navigateToSearchResult(group, group.results[0]);
return true;
}By definition — resolve it from the live tab.settingItems at reveal time, then navigate and
scroll:
function locate(
items: SettingDefinitionItem[],
name: string,
path: string[] = [],
): { definition: SettingDefinition; pagePath: string[] } | null {
for (const item of items) {
if (!("type" in item)) {
if (item.name === name) return { definition: item, pagePath: path };
continue;
}
const nested = item.type === "page" ? [...path, item.name] : path;
const hit = item.items && locate(item.items, name, nested);
if (hit) return hit;
}
return null;
}
function revealSetting(app: App, tabId: string, name: string): boolean {
app.setting.open();
const tab = app.setting.openTabById(tabId);
if (!tab) return false;
const hit = locate(tab.settingItems, name);
if (!hit) return false;
app.setting.navigateToSearchResult({ tab, pagePath: hit.pagePath });
app.setting.scrollToDefinition(tab, hit.definition);
return true;
}Resolve the definition fresh on every reveal. update() re-runs getSettingDefinitions() and
produces new objects, so a cached reference stops matching. scrollToDefinition also searches only
the rendered tab or sub-page, which is why the navigateToSearchResult call comes first.
app.setting.open();
const tab = app.setting.openTabById("hotkeys") as (SettingTab & { setQuery(q: string): void }) | null;
tab?.setQuery(plugin.manifest.id);Since 1.13.4 open() renders the modal in its own Electron window whenever the
settingsPopoutWindow config is on, which is the default. A harness that evaluates JS in the main
window and captures it — /obsidian-debug drives one — then reports an app with no settings in
it. Put the modal back in the main window for the duration:
app.vault.setConfig("settingsPopoutWindow", false);That persists to the vault's .obsidian/app.json, and the next open() honours it with no
reload — close() then open() re-homes a modal that is already up. The modal then answers
document.querySelector(".modal.mod-settings") and lands in a plain window capture. open()
stays synchronous in both modes, so the openTabById on the next line still finds its tab. The
Community plugins browser reads the same key, so it follows settings into whichever window they use.
To check how settings render in their own window, leave the config on and reach that window from the main renderer:
app.setting.isInPopoutWindow(); // false while the modal renders in the main window
const win = app.setting.getPopoutWindow();
win.document.querySelector(".vertical-tab-header");
win.getComputedStyle(el);
win.electronWindow.webContents
.capturePage()
.then((img) => require("fs").writeFileSync("<abs>.png", img.toPNG()));That window is a Modal popout rather than a WorkspaceWindow, so it stays out of
app.workspace.floatingSplit and fires no window-open event — app.setting is the only handle
on it. Its body carries is-popout-modal on top of the is-popout-window every popout gets.
While it is open the main renderer's activeWindow / activeDocument globals point at the
settings window, so reach the main window as window / document.
The reveal flash expires before a capture round-trip finishes, so assert is-flashing on the row
in the DOM instead of looking for it in a screenshot.
| API | Risk |
|---|---|
open(), close(), openTabById() | Safe — stable since ~0.9, used by the app:open-settings command. Handle the null return. |
activeTab, SettingTab.id / .name | Low — plain fields. Treat as read-only. |
navigateToSearchResult(), scrollToDefinition() | Medium — new in 1.13 with one core caller each. Guard with ?. and keep calls in one helper. |
searchIndex.search() | Medium — internal class returning bare object literals; a plausible refactor target. |
| Anything keyed by a setting id | Nonexistent — no id concept exists. |
There is no obsidian://settings URI and no per-setting command. For a deep link, register a handler
and dispatch to the helpers above:
this.registerObsidianProtocolHandler("zotlit-settings", ({ page, setting: name }) => {
if (name) revealSetting(this.app, this.manifest.id, name);
else openSettingsPage(this.app, this.manifest.id, page ? page.split("/") : []);
});control does this automatically; in render, call saveData()
from onChange.desc short — one sentence. Put warnings in a Modal with explicit confirm.In this codebase, settings tabs use the declarative API with control keys that bridge to
SettingsService. The imperative dual-support UI lives under setting-tab/compat/ for
Obsidian < 1.13 (minAppVersion 1.12.7), decoupled for easy removal.
When adding a setting:
getSettingDefinitions().setting-tab/compat/.references/settings-guide.md — full developer docs with all patterns and examplesreferences/migration-guide.md — migrating from imperative display() to declarativepackages/obsidian-api/obsidian.d.ts — canonical type definitions (grep for SettingDefinition,
SettingControl, PluginSettingTab, SettingPage)© aidenlx, AGPL-3.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
SKILL.md and 2 other files (references) in .agents/skills/obsidian-settings of aidenlx/zotlit.
Open the folder on GitHubat commit 27f5752
Obsidian Settings next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Obsidian Settings this skillaidenlx/zotlit | 1k | — | ~5.3k | Automated safety check: Pass | AGPL-3.0 | |
| Vault Setupearlyaidopters/second-brain | 193 | — | ~1.4k | Automated safety check: Pass | None | |
| Pa Plugin Config Reviewedonyzpc/personal-assistant | 148 | — | ~238 | Automated safety check: Pass | AGPL-3.0 | |
| Obsidian Local Dev Loopjeremylongshore/tons-of-skills-marketplace | 2.8k | — | ~2.4k | Automated safety check: Pass | MIT | |
| Obsidian Webhooks Eventsjeremylongshore/tons-of-skills-marketplace | 2.8k | — | ~3k | Automated safety check: Pass | MIT | |
| Hook Development for Claude Code Pluginsanthropics/claude-plugins-official | 38k | 10 repos | ~4.1k | Automated safety check: Notes | Apache-2.0 |
earlyaidopters/second-brain
Interactive Obsidian vault configurator. An agent skill from earlyaidopters/second-brain.
edonyzpc/personal-assistant
A skill your agent uses when inspecting Obsidian plugin lists, plugin settings, disabled plugins, config folders, or possible unused plugin signals.
jeremylongshore/tons-of-skills-marketplace
Set up a fast Obsidian plugin development loop with hot reload.
jeremylongshore/tons-of-skills-marketplace
Handle Obsidian events and workspace callbacks for plugin development.
anthropics/claude-plugins-official
Explains how to write Claude Code plugin hooks, both prompt-based checks and bash commands, for events such as PreToolUse, Stop and SessionStart.
anthropics/claude-plugins-official
Explains how to write agents for Claude Code plugins: the markdown file with YAML frontmatter, trigger descriptions, model and color settings, and system prompt design.
aidenlx/zotlit
Obsidian house style for the wording of user-facing UI strings — command names, setting labels, button text, notices, modal copy.
aidenlx/zotlit
Style Obsidian plugin UI with Tailwind + native components. An agent skill from aidenlx/zotlit.
aidenlx/zotlit
Typed regex authoring with arkregex in this repo. An agent skill from aidenlx/zotlit.
aidenlx/zotlit
Write a user-facing changelog entry under apps/docs/content/changelog/.
aidenlx/zotlit
Draft a Discord announcement from a changelog entry. An agent skill from aidenlx/zotlit.
aidenlx/zotlit
Define ZotLit UI messages in the Inlang Message Format and consume them through the generated JSON Language Pack facade.
Works with
Categories
Write declarative settings tabs for Obsidian plugins using the 1.13.0+ getSettingDefinitions() API. Obsidian Settings is an agent skill from aidenlx/zotlit.0+ getSettingDefinitions() API.
Obsidian Settings fits situations like: editing a PluginSettingTab; adding settings controls (toggle; building settings groups/lists/sub-pages; wiring conditional visibility.
Run `npx skills add aidenlx/zotlit --skill obsidian-settings -a claude-code`. Or copy the skill folder (.agents/skills/obsidian-settings in aidenlx/zotlit) into .claude/skills/obsidian-settings in your project. Claude Code loads it when a task matches its description.
Run `npx skills add aidenlx/zotlit --skill obsidian-settings -a codex`. Or copy the skill folder (.agents/skills/obsidian-settings in aidenlx/zotlit) into .agents/skills/obsidian-settings in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add aidenlx/zotlit --skill obsidian-settings -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/obsidian-settings, .gemini/skills/obsidian-settings, .github/skills/obsidian-settings and .opencode/skills/obsidian-settings in your project.
Going by SKILL.md and its folder, Obsidian Settings needs the command-line tools its instructions call (rg).
SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Obsidian Settings is published under the AGPL-3.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 5.3k tokens (SKILL.md is roughly 21k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full. Its references folder adds about 13k tokens, read only when the agent opens those files.
Skills that share tags, products or a category with Obsidian Settings: Vault Setup (earlyaidopters/second-brain, 193 stars), Pa Plugin Config Review (edonyzpc/personal-assistant, 148 stars), Obsidian Local Dev Loop (jeremylongshore/tons-of-skills-marketplace, 2.8k stars) and Obsidian Webhooks Events (jeremylongshore/tons-of-skills-marketplace, 2.8k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
aidenlx (a GitHub user) maintains it in aidenlx/zotlit, which has 1,028 GitHub stars. The repository holds 16 skills in this directory. The repository was last updated on October 9, 2026.
Source: aidenlx/zotlit on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.