Back to skill

Security audit

Conversation Recap to Obsidian

Security checks for vulnerabilities and agentic risk

Overview

The skill matches its Obsidian note-summary purpose, but its script can overwrite notes and can be configured to read or write outside the intended vault.

Install only if you are comfortable with a local script reading and overwriting Obsidian daily and weekly notes. Use explicit commands rather than generic words, keep config.json private and trusted, set obsidian_bin to the real Obsidian CLI only, and avoid absolute or ../ paths in daily_dir and weekly_dir until containment checks and safer overwrite behavior are added.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (2)

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/recap_manager.py:61
Finding

Configurable note directories can escape the configured Obsidian vault

Content
View full analysis
Path | None: root = resolve_vault_root(config) if root is None: return None return root / Path(path) ``` ```python def daily_path(config: Dict[str, Any], date: dt.date) -> str: base = config['daily_dir'].rstrip('/') return f"{base}/{date.year}/{date.month:02d}/{date.isoformat()}.md" def weekly_path(config: Dict[str, Any], sunday: dt.date) -> str: base = config['weekly_dir'].rstrip('/') return f"{base}/{sunday.year}/{sunday.month:02d}/{sunday.isoformat()}.md" ``` ### Technical Analysis The `daily_dir` and `weekly_dir` settings can be supplied through command-line arguments or `config.json`. Their values are concatenated into note paths without rejecting absolute paths, parent-directory traversal components, or symlink-based escapes. When `path` is absolute, `root / Path(path)` discards `root` and returns the absolute path. A relative value containing `../` can similarly resolve outside the vault. The code does not call `resolve()` and does not verify that the resulting target remains beneath the resolved vault root. The affected path is subsequently used for file reads, lock-file creation, directory creation, and note overwrites. The same untrusted logical path is also passed to the configurable Obsidian CLI. ### Attack Path 1. An attacker or untrusted automation modifies local configuration or supplies an argument such as: ```bash python scripts/recap_manager.py refresh-daily-auto \ --vault-path /home/user/vault \ --daily-dir ../../external-notes \ --date 2026-03-25 ``` 2. `daily_path()` creates a path containing the unvalidated traversal sequence. 3. `note_fs_path()` joins that ...[truncated 1009 chars]
Remediation
View remediation
Path: root = resolve_vault_root(config) if root is None: raise RuntimeError("Vault root is unresolved") logical = Path(path) if logical.is_absolute() or ".." in logical.parts: raise ValueError(f"Unsafe note path: {path}") resolved_root = root.expanduser().resolve(strict=True) target = (resolved_root / logical).resolve(strict=False) if not target.is_relative_to(resolved_root): raise ValueError(f"Note path escapes vault: {path}") return target ``` 4. Perform the containment check before reads, writes, lock-file creation, or parent-directory creation. 5. Define whether symlinks are permitted. If they are not required, reject symlinked path components or verify the resolved target remains in the vault immediately before every filesystem operation. 6. Validate paths sent to the Obsidian CLI using the same policy rather than relying only on direct-filesystem fallback checks. 7. Add tests covering absolute paths, `../` traversal, nested traversal, symlink escapes, and valid in-vault paths. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/recap_manager.py:268
Finding

Ambiguous summary markers and fail-open read handling can destroy existing note content

Content
View full analysis
tuple[int, int] | None: first_start = text.find(START) if first_start == -1: return None last_end = text.rfind(END) if last_end == -1 or last_end < first_start: return first_start, len(text) return first_start, last_end + len(END) def replace_summary_block(original: str, new_block: str) -> str: wrapped = f"{START}\n{new_block.strip()}\n{END}" region = find_generated_region(original) if region: start_idx, end_idx = region before = original[:start_idx].rstrip() after = original[end_idx:].strip() parts = [part for part in (before, wrapped, after) if part] return "\n\n".join(parts).rstrip() + "\n" sep = "\n\n" if original.rstrip() else "" return original.rstrip() + sep + wrapped + "\n" ``` ```python def cmd_append_entry(args): config = load_config(args) date = parse_date(args.date) if args.date else dt.date.today() path = daily_path(config, date) with note_lock(config, path): try: existing = read_note(config, path) except Exception: existing = '' normalized = normalize_daily_note(existing, date) updated = insert_before_summary_block(normalized, build_entry_block(args)) create_or_overwrite_note(config, path, normalize_daily_note(updated, date)) print(path) def cmd_refresh_daily_auto(args): config = load_config(args) date = parse_date(args.date) if args.date else dt.date.today() path = daily_path(config, date) with note_lock(config, path): try: existing = read_note(config, path) except Exception: existing = '' ...[truncated 3172 chars]
Remediation
View remediation
Vulnerability Patterns
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
Findings (10)

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 218)May include surrounding context.

md
- `scripts/recap_manager.py`

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 231)May include surrounding context.

md
- `scripts/recap_manager.py`

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 244)May include surrounding context.

md
- `scripts/recap_manager.py`

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 250)May include surrounding context.

md
- `scripts/recap_manager.py`

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The documented triggers are extremely generic natural-language words ('summary', 'daily', 'weekly') that can easily appear in ordinary conversation. In an agent skill context, this can cause unintended activation, leading the skill to read notes, regenerate summaries, or append content when the user was not actually invoking the tool.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding

The skill instructs use of a bundled script and describes file read, file write, and shell-based workflows, but it declares no explicit tool scope or permissions boundary. That creates an authorization gap where an agent may invoke powerful capabilities on local notes or execute commands without a least-privilege contract, increasing the chance of unintended file modification or command misuse.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

The activation text is very broad ('use this skill whenever the user asks to summarize...') and covers many adjacent tasks, which can cause over-triggering in contexts the user did not clearly intend. In practice this can redirect ordinary summarization requests into file-affecting Obsidian workflows, increasing the risk of unwanted note reads, writes, or script execution.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

Multiple required output templates and guidance are written as Chinese field labels and examples, such as '事项标题', '问题', '方案', '结论', and '今日总结', indicating a fixed language/locale expectation. The file does not state that Chinese is optional, user-selected, or required for a region-specific purpose, so this constitutes a language-policy issue.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
93% confidence
Finding

The code executes an external program from configurable input via config['obsidian_bin'], which can come from CLI arguments or a JSON config file. Although subprocess.run is used without a shell, avoiding classic shell injection, this still allows arbitrary executable selection and argument delivery to that executable, resulting in command execution if an attacker can influence configuration or invocation.

Content

Scanner excerpt · scripts/recap_manager.py (reported line 101)May include surrounding context.

python
def run_obsidian(config: Dict[str, Any], args: List[str]) -> str:
    cmd = [config['obsidian_bin'], *args]
    res = subprocess.run(cmd, capture_output=True, text=True)
    output = res.stdout or ''
    if res.returncode != 0 or is_error_output(output):
        raise RuntimeError(res.stderr.strip() or res.stdout.strip() or f"command failed: {' '.join(cmd)}")

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Confidence
50% confidence
Finding

Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Content

Scanner excerpt · scripts/recap_manager.py (reported line 42)May include surrounding context.

python
except Exception:
            pass
    for key in ('obsidian_bin', 'vault', 'vault_path', 'daily_dir', 'weekly_dir'):
        val = getattr(cli_args, key, None)
        if val:
            config[key] = val
    return config

Static analysis

No suspicious patterns detected.