| name | decide |
| description | Captures architectural and technical decisions as ADR-lite notes in the Obsidian vault. Use when: (1) /decide command to record a decision from the current session, (2) /decide <decision summary> to capture a specific decision, (3) user wants to document why a particular technical choice was made. |
| metadata | {"version":"1.0.0"} |
Decide โ Record Technical Decisions to Obsidian
Capture architectural and technical decisions with full context as ADR-lite (Architecture Decision Record) notes in the Obsidian vault. Each decision records the context, options considered, rationale, and trade-offs accepted.
Tools needed: Bash, Read
Procedure
Follow these steps exactly. Do not skip steps or reorder them.
Step 1 โ Read config
Run:
cd "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
python3 -c '
import sys, os
import glob, json, os, re, sys
def _ob_hooks():
try:
for _m in json.load(open(os.path.expanduser("~/.claude/plugins/known_marketplaces.json"))).values():
_s = _m.get("source") if isinstance(_m, dict) else None
if not (isinstance(_s, dict) and _s.get("source") == "directory"):
continue
_i = _m.get("installLocation") if isinstance(_m, dict) else None
if not (isinstance(_i, str) and os.path.isabs(_i)):
continue
_h = os.path.join(_i, "hooks")
if os.path.isfile(os.path.join(_h, "obsidian_utils.py")):
return _h
except Exception:
pass
_c = [_d for _d in glob.glob(os.path.expanduser("~/.claude/plugins/cache/*/obsidian-brain/*/hooks")) if re.fullmatch("[0-9]+([.][0-9]+)*", _d.split("/")[-2])]
return max(_c, key=lambda _p: ([int(_n) for _n in _p.split("/")[-2].split(".")], _p), default="hooks")
sys.path.insert(0, _ob_hooks())
from obsidian_utils import load_config
c = load_config()
if not c.get("vault_path"):
print("ERROR: vault_path not configured", file=sys.stderr)
sys.exit(1)
print("VAULT=" + c["vault_path"])
print("SESS=" + c.get("sessions_folder", "claude-sessions"))
print("INS=" + c.get("insights_folder", "claude-insights"))
'
Parse each output line as KEY=VALUE, splitting on the first =.
If the file does not exist or is invalid JSON, tell the user:
Config not found. Please run /obsidian-setup first to configure your Obsidian vault.
Stop here if config is missing.
Step 2 โ Validate vault access
Run:
test -d "$VAULT_PATH/$INSIGHTS_FOLDER" && test -w "$VAULT_PATH/$INSIGHTS_FOLDER" && echo "OK" || echo "FAIL"
If FAIL, tell the user:
The insights folder $VAULT_PATH/$INSIGHTS_FOLDER does not exist or is not writable. Run /obsidian-setup to fix this.
Stop here if FAIL.
Step 3 โ Identify the decision
Check if the user provided an argument after /decide.
- With argument (e.g.
/decide use Redis for rate limiting): Use the argument as the decision summary and analyze the conversation for supporting context around that decision.
- Without argument (bare
/decide): Analyze the full conversation to identify the most recent or most significant architectural/technical decision made during the session. Present it to the user for confirmation:
Decision detected:
Is this the decision you want to record, or would you like to specify a different one?
Wait for confirmation before proceeding.
Step 4 โ Structure the ADR-lite note
Analyze the conversation thoroughly and draft the decision note with these sections:
Context โ 2-4 sentences explaining what problem or situation prompted this decision. What were the requirements or constraints?
Options Considered โ A numbered list of alternatives that were discussed or could have been considered. For each option, include a brief description (1-2 sentences).
Decision โ A clear statement of what was chosen. 1-3 sentences.
Rationale โ Why this option was selected over the alternatives. What factors tipped the balance? 2-4 sentences.
Consequences โ What trade-offs were accepted by making this decision. Include both positive outcomes and negative trade-offs. Bulleted list of 2-5 items.
Step 5 โ Auto-generate topic tags
Based on the decision content, generate 1-3 topic tags. Tags should be lowercase, hyphenated, and specific. Examples:
claude/topic/caching-strategy
claude/topic/database-choice
claude/topic/api-design
claude/topic/auth-architecture
Step 6 โ Show preview and ask for edits
Present the full note to the user including frontmatter:
---
type: claude-decision
date: YYYY-MM-DD
created_at: <ISO-8601-UTC>
source_session: <current-session-id>
source_session_note: "[[<session-note-filename>]]"
project: <project-name>
status: active
tags:
- claude/decision
- claude/project/<project-name>
- claude/topic/<auto-generated-topic-1>
- claude/topic/<auto-generated-topic-2>
---
# <Decision Title>
## Context
<What problem prompted this decision>
## Options Considered
1. **<Option A>** โ <brief description>
2. **<Option B>** โ <brief description>
3. **<Option C>** โ <brief description>
## Decision
<What was chosen>
## Rationale
<Why this option won>
## Consequences
- <positive or negative trade-off>
- <positive or negative trade-off>
- <positive or negative trade-off>
Where:
-
YYYY-MM-DD is today's date
-
<ISO-8601-UTC> is the current UTC timestamp at second precision. Get it via:
python3 -c 'from datetime import datetime, timezone; print(datetime.now(timezone.utc).isoformat(timespec="seconds"))'
Example: 2026-04-24T18:42:11+00:00
-
<current-session-id> and <session-note-filename> are derived together. Get session context via the shared helper:
cd "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
python3 -c '
import sys, os
import glob, json, os, re, sys
def _ob_hooks():
try:
for _m in json.load(open(os.path.expanduser("~/.claude/plugins/known_marketplaces.json"))).values():
_s = _m.get("source") if isinstance(_m, dict) else None
if not (isinstance(_s, dict) and _s.get("source") == "directory"):
continue
_i = _m.get("installLocation") if isinstance(_m, dict) else None
if not (isinstance(_i, str) and os.path.isabs(_i)):
continue
_h = os.path.join(_i, "hooks")
if os.path.isfile(os.path.join(_h, "obsidian_utils.py")):
return _h
except Exception:
pass
_c = [_d for _d in glob.glob(os.path.expanduser("~/.claude/plugins/cache/*/obsidian-brain/*/hooks")) if re.fullmatch("[0-9]+([.][0-9]+)*", _d.split("/")[-2])]
return max(_c, key=lambda _p: ([int(_n) for _n in _p.split("/")[-2].split(".")], _p), default="hooks")
sys.path.insert(0, _ob_hooks())
from obsidian_utils import load_config, get_session_context
c = load_config()
ctx = get_session_context(c["vault_path"], c.get("sessions_folder", "claude-sessions"))
print("SID=" + ctx["session_id"] + " HASH=" + ctx["hash"] + " PROJECT=" + ctx["project"] + " SESSION_NOTE=" + ctx["session_note_name"])
'
Parse the output to get SESSION_ID, HASH, PROJECT, and SESSION_NOTE.
Important: If SESSION_ID is unknown, use unknown for source_session and omit source_session_note entirely.
-
<project-name> is the PROJECT value from get_session_context() (lowercased, hyphenated basename of cwd)
-
The source_session_note field creates an Obsidian backlink to the source session note
Ask the user:
Preview above. Would you like to:
- save as-is
- edit tags โ add or remove tags
- edit content โ tell me what to change
- cancel โ discard this decision
Wait for the user's response. Apply any requested edits and show the updated preview. Repeat until the user says save or cancel.
If cancel, stop here.
Step 7 โ Generate filename
Construct the filename from these parts:
- Date:
YYYY-MM-DD (today)
- Slug: The decision title, lowercased, spaces replaced with hyphens, non-alphanumeric characters (except hyphens) removed, truncated to 50 characters
- Hash: 4-character hex hash derived from the current timestamp:
date +%s | md5 | cut -c29-32 (macOS) or date +%s | md5sum | cut -c1-4 (Linux). Do NOT use tail -c 4 โ it counts the trailing newline as a byte and returns only 3 visible characters.
- Suffix:
-decision
Final filename: YYYY-MM-DD-<slug>-<hash>-decision.md
Example: 2026-04-04-use-redis-for-rate-limiting-a3f2-decision.md
Step 8 โ Write the note
Run the note-writer CLI, piping the full note (frontmatter + body) in on stdin. It creates $INSIGHTS_FOLDER if needed and writes the file atomically at mode 0o600 โ no mkdir/chmod needed. Two rules for the heredoc terminator, both load-bearing. (1) It must stay quoted (<<'OB_NOTE_EOF_<eof4>') โ do not drop the quotes in a future edit. (2) It must be unique per invocation: substitute the same 4 random hex characters for <eof4> in BOTH the <<'OB_NOTE_EOF_<eof4>' opener and the terminator line, then confirm that no line of the content you are about to emit is exactly that terminator โ if one is, pick different hex characters and re-check. Never replace this with a fixed delimiter. Quoting stops $/backtick expansion but does NOT stop early termination: a line equal to the terminator at column 0 ends the heredoc there, silently truncating the content AND handing everything after it to the shell as commands to execute. Notes written by this plugin routinely quote these very blocks, so a fixed terminator is a live hazard, not a theoretical one. Self-check before you emit the block: if the terminator still contains < or >, you have not substituted it. Stop and substitute it โ the literal <eof4> form appears at column 0 inside these SKILL.md blocks themselves, so a note quoting one of them collides all over again, and nothing on the shell side can catch that. The HOOKS= line below checks the marketplace-registered directory-source install location FIRST (#278 โ on a local checkout that is what loads, not the released cache), and only falls back to the plugin cache, where it sorts versions numerically (a plain max() is lexicographic and picks 3.9.0 over 3.10.0, resolving to a cache with no note_writer.py); the test -f line turns a stale/incomplete cache into the documented ERROR: shape instead of a raw Python can't open file message. An unquoted delimiter lets the shell expand $ variables and backtick commands embedded in the note body, silently corrupting it:
cd "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
HOOKS=$(python3 -c "
import glob, json, os, re
def _ob_hooks():
try:
for _m in json.load(open(os.path.expanduser('~/.claude/plugins/known_marketplaces.json'))).values():
_s = _m.get('source') if isinstance(_m, dict) else None
if not (isinstance(_s, dict) and _s.get('source') == 'directory'):
continue
_i = _m.get('installLocation') if isinstance(_m, dict) else None
if not (isinstance(_i, str) and os.path.isabs(_i)):
continue
_h = os.path.join(_i, 'hooks')
if os.path.isfile(os.path.join(_h, 'obsidian_utils.py')):
return _h
except Exception:
pass
_c = [_d for _d in glob.glob(os.path.expanduser('~/.claude/plugins/cache/*/obsidian-brain/*/hooks')) if re.fullmatch('[0-9]+([.][0-9]+)*', _d.split('/')[-2])]
return max(_c, key=lambda _p: ([int(_n) for _n in _p.split('/')[-2].split('.')], _p), default='hooks')
print(_ob_hooks())
")
test -f "$HOOKS/note_writer.py" || { echo "ERROR: note_writer.py not found under $HOOKS - resolution checks the marketplace registered install location first, then falls back to the plugin cache; neither path produced a hooks directory containing it. Verify the obsidian-brain install resolved at $HOOKS is complete (git pull for a directory-source checkout, or run /plugin marketplace update for a cache install), then retry." >&2; exit 1; }
python3 "$HOOKS/note_writer.py" write "$VAULT_PATH" "$INSIGHTS_FOLDER" "YYYY-MM-DD-<slug>-<hash>-decision.md" <<'OB_NOTE_EOF_<eof4>'
---
type: claude-decision
...
---
...
OB_NOTE_EOF_<eof4>
On success this prints OK: <absolute path> โ that is the file at $VAULT_PATH/$INSIGHTS_FOLDER/<filename>. On failure it prints ERROR: <reason> to stderr and exits non-zero; surface that message to the user and stop here.
If the error is note already exists, the 4-hex filename hash collided with a note written in the same second. Regenerate the hash (Step 7's command), rebuild the filename, and retry the write once. If it fails again for any reason, surface the error and stop โ do not loop.
Step 9 โ Confirm
Print:
Decision recorded!
- File:
$VAULT_PATH/$INSIGHTS_FOLDER/<filename>
- Status:
active
- Tags:
claude/decision, claude/project/<name>, claude/topic/<topic1>, ...
- Open in Obsidian to view, link to other notes, or update the status later.
To revisit past decisions, search for type: claude-decision in your vault or check the Active Decisions section of the sessions-overview dashboard.