Install
$ agentstack add skill-ozlar34-claude-code-skills-triage ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Triage Skill
Triage items the user dropped into the canonical inbox — web clips, Raindrop exports, manual pastes, or Telegram voice/text captures. Triage is mechanical, one file at a time — verdict → promote → flip → archive.
This skill is a verdict-loop closer — verdict → promote → flip → archive. Unlike a pure frontmatter-flip closer, clippings carry a destructive op at the end (archive mv or rm), so the atomicity ordering is promote → flip → destruct. The flip is the stamp that says "if my next step fails, the queue can resume here without re-triaging." One verdict — hold — is exempt: it deliberately keeps a clipping in Inbox/ untouched for a future run, performing no promote, flip, or destruct. It is the supported way to say "decide this later" without silently abandoning the file.
Modes & routing
/triage has three modes. Parse the skill argument first and branch:
| Argument | Mode | What runs | |----------|------|-----------| | propose (aliases: scan, batch, async) | A — Propose | Fan out one read-only subagent per clipping → stage proposals to .triage/pending.json → push a summary to Telegram. Writes nothing to the vault. Jump to Mode A — Propose. | | review (aliases: execute, apply, confirm) | B — Review | Load .triage/pending.json → walk proposals (accept-all default) → run the real promote→flip→destruct on confirmed verdicts. Jump to Mode B — Review. | | (empty) | C — Interactive | The legacy per-clipping walkthrough below, bounded to 2 actionable verdicts. But first: if .triage/pending.json exists and is non-empty, tell the user a pending proposal queue exists and offer to run Review instead. |
Modes A and B are a propose-then-execute split: A is the autonomous analytical pass the user kicks off whenever (then walks away — his phone gets the action list), B is the fast confirm pass that actually empties the inbox. C is the original all-in-one interactive flow, untouched.
The split exists because the analytical work (read + fit-scan + enrich) is what's slow and benefits from per-clipping context isolation, while the destructive work (mv/rm/flip) is what must never happen without the user's confirm. A separates the first from the second; B gates the second.
Why subagents and not a Workflow: at typical inbox volume (a handful at a time) plain parallel subagents give the context-isolation win without the Workflow harness's filesystem dance. If inbox volume ever grows past ~15 routinely, the clean upgrade is to wrap Mode A's fan-out in a Workflow fanning agent() calls with the same schema — the analysis protocol file already is the agent spec.
Shared setup (path resolution below) runs for all three modes.
Source of truth: vault-paths.json
All vault paths come from .planning/vault-paths.json in the project dir. Never hard-code vault folder names. If the user has restructured the vault, the JSON is the only place that knows.
Keys consumed:
vault_root— absolute path to the Obsidian vault rootinbox— where unprocessed clippings live (the only place this skill pulls from)resources—Resources/(used only to compose resource-note bodies; actual write goes viaobsidian-write)areas—Areas/(used byact+backlogto list candidate area notes at runtime)system—_System/(also listed foract+backlog; the Tool Backlog at_System/Claude Code/Tool Backlog.mdis surfaced explicitly via$TOOL_BACKLOGsince it sits below the top-level glob)archive—Archive/(root of cold storage; this skill writes to/Source//)
Resolve at session start (before any read or write):
VAULT_PATHS="$HOME//.planning/vault-paths.json"
VAULT_ROOT=$(jq -er '.vault_root' "$VAULT_PATHS")
INBOX=$(jq -er '.inbox' "$VAULT_PATHS")
RESOURCES=$(jq -er '.resources' "$VAULT_PATHS")
AREAS=$(jq -er '.areas' "$VAULT_PATHS")
SYSTEM=$(jq -er '.system' "$VAULT_PATHS")
ARCHIVE=$(jq -er '.archive' "$VAULT_PATHS")
# `jq -er` (NOT `jq -r`): -e forces non-zero exit on missing/null key.
# The Tool Backlog (the CC-tool sink) lives ONE LEVEL DEEPER than the top-level
# `$SYSTEM/*.md` destination glob reaches — `_System/Claude Code/Tool Backlog.md` —
# so `act`/`backlog` never surface it and the proposer guesses a wrong path. Surface
# it explicitly. We do NOT glob `_System` recursively (that floods the candidate list
# with every `Tool Notes/.md`); the Tool Backlog is the one nested sink worth knowing.
TOOL_BACKLOG="$VAULT_ROOT/$SYSTEM/Claude Code/Tool Backlog.md"
# Staged-proposal queue (Modes A + B). A *working artifact*, NOT vault content —
# lives in the project dir (gitignored), survives a /clear between propose and review.
TRIAGE_STATE="$HOME//.triage"
PENDING="$TRIAGE_STATE/pending.json"
# Plugin/system files in Inbox/ that are never triage subjects — excluded from all ls snapshots
INBOX_EXCLUDE="_Triage Queue.md"
If vault-paths.json is missing OR any key is empty, stop and tell the user — do not guess paths.
ALWAYS write via tempfile-rename — protocol: ~//.claude/lib/write-discipline.md
The destructive ops (mv of the clipping to archive, rm for ignore) are already atomic on APFS — no .tmp indirection needed.
Session budget — every 2 actionable verdicts, hand off to a fresh session
This skill is bounded per invocation: it stops after 2 clippings receive an actionable verdict (act, save, backlog, or ignore). hold does NOT count — it is zero-write and produces no context bloat, so a single session can absorb arbitrarily many holds while still resolving up to 2 actionable items.
At the threshold, print a continuation prompt (Step 8.5) and stop the loop. The remaining inbox items will be picked up by the next /triage invocation from a cleared session.
Rationale: long triage runs accumulate verdict-by-verdict context that biases later proposals toward "I've already seen things like this, default to backlog." Bounding the session keeps every clipping seen with a fresh analytical lens and keeps token spend predictable.
Counter mechanic: initialise ACTIONABLE_COUNT=0 at the top of the snapshot loop. Increment it inside Step 6, but ONLY for the four actionable verdicts (act, save, backlog, ignore). hold is exempt — it skips Steps 4–7 entirely, so the counter never advances on a held clipping. After Step 8, Step 8.5 evaluates the threshold against ACTIONABLE_COUNT + remaining-inbox count.
Frontmatter contract (Phase 01 schema)
Every clipping in the inbox carries (or will carry, after you patch it) the Phase 01 frontmatter schema:
---
note_type: source # required; one of the Phase 01 enums
captured_from: # web-clipper | telegram | manual | raindrop
captured_at: 2026-05-19 # ISO date the clipping was captured
processed: false # flips to true the moment you commit a verdict (after promote, before mv)
tags: [...] # optional, from the clipper or added during triage
---
Atomicity rule (load-bearing): processed: true MUST be written to the clipping AFTER the promote step (area-note bullet / resource-note creation) and BEFORE any mv / rm operation. If the destructive op fails mid-flight, the next run sees a processed-but-still-in-inbox file and can recover by completing the move, rather than silently re-triaging. The promote step precedes the flip because the promote is itself recoverable (a duplicate bullet is annoying but not destructive), but if it fails AFTER the flip, the clipping would be marked processed without the back-link in place. The hold verdict sits outside this ordering entirely — it performs no promote, no flip, and no destructive op, so there is no atomicity concern; the clipping is left exactly as-is.
The 5-verdict matrix
For each clipping, choose exactly one verdict. Verdicts are mutually exclusive — never combine.
| Verdict | Meaning | Promote (Step 4) | Destructive op (Step 6) | | --------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | | act | This clipping triggers a concrete next action right now. | Append a bullet to a user-chosen area note's section (default ## Notes); optional TickTick task at Step 7. | mv to archive | | save | Durable reference value — worth a clean resource note for future retrieval. Proposer classifies a save_subtype: reference, person, place, or long-form. | Forks on save_subtype (proposer-supplied; in Mode C derived from the Step 2c proposal): reference → obsidian-write (mode=create-new), Naming Gate + Resource→Area auto-backlink (current path); person → direct Write to People/.md, create-or-append per save-routing.md; place → direct Write to Places/.md, create-or-append per save-routing.md; long-form → spawn a /summarize sub-agent (minimal mode) that mints the summary note inline; no TickTick task. All four sub-routes keep verdict save. | mv to archive | | backlog | Useful-but-not-now — worth keeping for later, but not yet a clean note. | Append a bullet to a user-chosen area note's ## Backlog section (auto-create section if missing). | mv to archive | | ignore | Noise, duplicate, stale link, or context the future you won't need. | None. | rm (no archive copy) | | hold | Decision consciously deferred — keep this clipping in Inbox/ for a future run. | None. | None — file stays in Inbox/, byte-identical, processed: false |
Four of the five verdicts (act, save, backlog, ignore) end with the file out of Inbox/ and processed: true written. hold is the deliberate fifth path: the clipping stays in Inbox/, byte-identical, processed: false, to be re-surfaced in the next run. hold is a conscious deferral chosen at Step 3 — not an accidental escape hatch. Every clipping must still receive an explicit verdict; what's forbidden is silently abandoning a file mid-loop. If a clipping resists classification, either ask the user for a real verdict or choose hold deliberately.
Archive destination shape
For act, save, and backlog verdicts:
$VAULT_ROOT/$ARCHIVE/Source//.md
Sourceis a fixed subfolder name (cold storage of triaged source material — distinct fromsource::as a frontmatter field)- `
is the year fromcaptured_at` if present, else current year - Filename is preserved verbatim (no rename) so existing back-links keep resolving
- Create the year folder if it doesn't exist (
mkdir -p)
Compute the archive path BEFORE any area-note write or resource-note composition, so the back-links can reference the future archive location and resolve once the mv completes at Step 6.
Mode C — Interactive (operating procedure, per clipping)
This is the original all-in-one flow, reached by bare /triage (no argument). Modes A and B reuse its building blocks: Mode A's subagents borrow the Step 2a fit-scan logic (read-only, via the analysis-protocol reference), and Mode B's executor reuses Steps 4–7 verbatim to apply a confirmed verdict. Keep those steps the single source of truth — don't fork the promote/flip/ destruct logic into the new modes.
For each *.md file currently in $VAULT_ROOT/$INBOX/:
Step 1 — Discover
ls "$VAULT_ROOT/$INBOX/"*.md 2>/dev/null | grep -v "$INBOX_EXCLUDE"
If the directory is empty, print "No clippings to triage." and stop. Otherwise, snapshot this file list once at the start of the run and iterate the snapshot — don't re-ls between clippings. This matters because of hold: a hold verdict leaves its file in Inbox/, so a fresh ls mid-run would re-surface a just-held file in an endless loop. Iterating a fixed snapshot guarantees each clipping is presented exactly once per run; a held file simply reappears in the next run's snapshot. Process the snapshot by modification time (oldest first).
If the file's frontmatter already shows processed: true, this is a recovery case (a prior run flipped but failed to mv). Skip to Step 6 — compute the archive path and complete the move. Do not re-promote or re-prompt.
Step 2a — Full read + system-fit scan
Read the full clipping body (no truncation — the fit-scan needs the whole thing; truncation is the root cause of "everything looks like backlog").
Resolve the system-fit signals once per session and cache them for the snapshot — do not re-ls per clipping:
RESOURCE_BUCKETS=$(ls -1 "$VAULT_ROOT/$RESOURCES" 2>/dev/null)
# sed, NOT `xargs basename`: the vault root may contain an apostrophe,
# which makes `xargs -I{} basename` fail with "unterminated quote" and silently empties the list.
AREA_NOTES=$(ls -1 "$VAULT_ROOT/$AREAS"/*.md "$VAULT_ROOT/$SYSTEM"/*.md "$TOOL_BACKLOG" 2>/dev/null | sed -E 's#.*/##; s#\.md$##')
RESOURCE_BUCKETS currently resolves to subfolders like Career, Citizenship, Claude, Coffee, Finance, German B1, Health, Home Office, Travel — these are the buckets a save verdict would route into. AREA_NOTES are the candidate destinations for act and backlog.
Classify the clipping into one of three fit buckets by cross-referencing its body, title, tags, and (if web-clipper / raindrop / manual) URL against the cached signals:
- clear-fit — body clearly matches at least one known Resources subfolder OR Area note. Name the match in the display (e.g. "topic = Coffee → fits
Resources/Coffee/andAreas/Coffee Hub.md"). - weak-fit — body has partial topical overlap but no obvious bucket; needs investigation before a confident verdict.
- no-fit — empty, garbage, dead URL, off-topic from every tracked domain. Default verdict will be
ignore.
Display: title (or first sentence for telegram), captured_from, captured_at, fit bucket + named match. Branch source-specifically on display:
captured_from: telegram: Telegram captures are short transcribed thoughts — no title, URL, or author. Frame the fit-scan around the thought itself ("this thought fits Areas/" / "this thought is about , no clear bucket" / "noise"). Do not attempt to extract a title or source URL; they won't be present.
captured_from: web-clipper | raindrop | manual: Frame the fit-scan as an external reference fitting (or not fitting) a destination.
Step 2b — Optional enrichment (smart default with override)
Call AskUserQuestion with two options. The default depends on the fit bucket from Step 2a:
| Fit bucket | Default option | Why | |------------|----------------|-----| | clear-fit | Skip enrichment (default) | Bucket is obvious; enrichment rarely changes the verdict. | | weak-fit | Enrich (default) | This is where enrichment pays off — disambiguate the fit. | | no-fit | Skip enrichment (default) | Heading for ignore anyway; no point researching. |
Both options must always appear so the user can override the smart default. Phrase the question so the default is named, e.g. on a weak-fit clipping:
> "Enrich context bef
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: ozlar34
- Source: ozlar34/claude-code-skills
- License: MIT
- Homepage: https://github.com/ozlar34
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.