feat: add /vault-cli:session-close command#17
Conversation
Port end-of-session safety check from ~/.claude/commands/session-close.md into the vault-cli plugin so coworkers installing vault-cli get the per-session lifecycle bookend alongside per-task and per-day trinities. All vault-specific paths driven by `vault-cli config` (tasks_dir, goals_dir, daily_dir, knowledge_dir); runbook folders auto-discovered via `^[0-9]+ [Rr]unbooks$` regex (no config field). Cross-surface checks (git, dark-factory, gh, TaskList) degrade silently when absent. Verification grep returns no hardcoded vault paths or org-specific identifiers in the command body.
There was a problem hiding this comment.
Now let me review the key concerns from the plan systematically.
Reviewed files:
commands/session-close.md— new 321-line slash commandCHANGELOG.md— added unreleased entryREADME.md— added two new command entries
Findings:
Security
- Tool allowlist — The
vault-cli:*,git:*,gh:*,dark-factory:*,lsof:*,find:*,ps:*,grep:*,jobs:*,command -v:*wildcards are broad, but the command's behavior is read-only + ask (Phase 9 surfaces items, never auto-commits/kills/closes). Thevault-cli:*wildcard is acceptable for this vault lifecycle management command. No critical security issue. - Phase 3.5 sibling session detection via
lsof +D/ps -o pid,cwdis documented as having false negatives ("conservative test"). Acceptable tradeoff. Not a bug.
Correctness
- Phase 3:
@{u}fallback false positive —git log --oneline @{u}.. 2>/dev/null || echo "no upstream / not pushed"will output the literal string"no upstream / not pushed"on branches without an upstream. This string is then counted as "unpushed commits" in downstream logic. This is a real correctness bug. A branch with no upstream (e.g., a local-only feature branch never pushed) would be incorrectly flagged as having unpushed commits. - Phase 8.6: single
head -1for orphan detection —grep ... | head -1finds at most one inbound linker. If a page has multiple inbound linkers and the first is the page itself, the orphan check could produce a false negative. Documented as a known limitation. Low severity — one match is sufficient for most use cases. - VAULT_CONFIG caching — Parsed once at command start. If
vault-cli configchanges mid-session, paths used are stale. This is standard caching behavior and is documented. Not a bug.
Testing
- No test file for the new command — Slash commands in
commands/are not tested viamake precommit. This is consistent with the project's existing pattern (other commands have no unit tests). Acceptable.
{
"verdict": "request-changes",
"summary": "The session-close slash command is well-structured and the allowlist is appropriate for its read+report+ask model. However, Phase 3 has a correctness bug: when a branch has no upstream configured, `git log @{u}..` fails silently and `echo \"no upstream / not pushed\"` is incorrectly treated as unpushed commits. Additionally, the changelog entry was placed under `## Unreleased` rather than `## v0.75.0`, inconsistent with the project's versioned-release changelog style.",
"comments": [
{
"file": "commands/session-close.md",
"line": 74,
"severity": "major",
"message": "Phase 3: `git log --oneline @{u}.. 2>/dev/null || echo \"no upstream / not pushed\"` — when @{u} has no upstream, the fallback echo is a plain string not an error. Downstream logic that counts \"lines\" from this output will miscount this as an unpushed commit. Fix: check upstream existence first with `git rev-parse --abbrev-ref @{u} 2>/dev/null`, or suppress the fallback echo entirely (only emit on actual unpushed commits)."
},
{
"file": "CHANGELOG.md",
"line": 11,
"severity": "minor",
"message": "Changelog entry is under `## Unreleased` (unreleased section). This project uses versioned releases — the entry should go under `## v0.75.0` (next minor bump from v0.74.0), consistent with the project's changelog convention. `## Unreleased` is typically reserved for unreleased changes in main, not in a feature branch PR."
}
],
"concerns_addressed": [
"security: tool allowlist is appropriate for read+report+ask command — no auto-mutations, vault-cli:* wildcard acceptable for lifecycle management",
"security: Phase 3.5 lsof +D sibling detection documented as conservative with false negatives — acceptable tradeoff, not a bug",
"correctness: Phase 1 scope detection — no issue found in command doc",
"correctness: Phase 3 git @{u}.. fallback — RAISED as major: false positive on no-upstream branches",
"correctness: Phase 3.5 orphan worktree detection — documented false negatives acceptable",
"correctness: Phase 8 reflect scoring thresholds — documented as arbitrary heuristic, acceptable for soft signal",
"correctness: Phase 8.6 single grep result — documented limitation, acceptable",
"correctness: VAULT_CONFIG cached — standard caching, documented, not a bug",
"correctness: Phase 8.5 conversation scan — documented expense, not a bug",
"tests: no test file — consistent with project pattern for slash commands, not a blocker"
]
}Replace `git log @{u}.. || echo "no upstream"` with an explicit upstream
check via `git rev-parse --abbrev-ref @{u}` before the log call. The
fallback echo string was ambiguous — a line-counting consumer would
miscount it as an unpushed commit. Empty output now unambiguously means
"caught-up OR no upstream" — both silent-OK states.
Addresses pr-reviewer MAJOR finding on PR #17.
superseded by new automated review
There was a problem hiding this comment.
{
"verdict": "request-changes",
"summary": "The session-close command implements its behavior correctly and has exemplary output format specification, but the allowed-tools Bash patterns are overly broad for a read-only observation command — git:*, gh:*, and dark-factory:* grant write operations (push, reset, repo delete, spec approve) that the command never performs. Additionally, at 324 lines the command is 3× the recommended 50–100 line size; Phase 8 scoring tables and Phase 3.5/8.6 detection algorithms should be extracted to a skill.",
"comments": [
{
"file": "commands/session-close.md",
"line": 10,
"severity": "major",
"message": "Bash(git:*) is too broad for a read-only command. Grants write operations (git push, git reset --hard, git push --force) that the command never performs. Should be narrowed to read-only subcommands: git status, git log, git rev-parse, git worktree, git ls-remote, git branch, git diff only."
},
{
"file": "commands/session-close.md",
"line": 11,
"severity": "major",
"message": "Bash(gh:*) is too broad. Allows gh repo delete, gh api (full API access), and other write operations. The command only reads PR state. Should be narrowed to Bash(gh pr:*) or Bash(gh pr view:*) specifically."
},
{
"file": "commands/session-close.md",
"line": 16,
"severity": "major",
"message": "Bash(dark-factory:*) is too broad. Allows dark-factory spec approve and dark-factory daemon (state-mutating) in addition to dark-factory status (read-only). The command only calls dark-factory status. Should be Bash(dark-factory status:*) or Bash(dark-factory:status) specifically."
},
{
"file": "commands/session-close.md",
"line": 1,
"severity": "nit",
"message": "Command is 324 lines, exceeding the 50–100 line guideline by 3×. Phase 8 scoring tables, Phase 3.5 orphan worktree detection, and Phase 8.6 link hygiene algorithms should be extracted to a skill invoked via the Skill tool, keeping the command to phase orchestration and output formatting only."
}
],
"concerns_addressed": [
"security: allowed-tools broad bash access — raised as major findings, patterns need narrowing to least-privilege read-only subsets",
"correctness: Phase 1 session scope detection relies on 'edits this session' — acknowledged in command design (must stay inline), sub-agent cannot see parent conversation",
"correctness: Phase 3 git log fallback to 'true' — the commit f3dbe83 adds upstream awareness, 19a8b05 fix confirmed upstream-aware fallback is in place",
"correctness: Phase 3.5 orphan worktree detection uses lsof +D (Linux-only) — documented as platform-specific in Phase 3.5 description, acceptable degradation",
"correctness: Phase 8 reflect scoring is heuristic-based — acknowledged in command, only surfaces suggestion not auto-invokes",
"correctness: Phase 8.6 link hygiene wikilink scope — correctly scoped to VAULT_CONFIG[].path paths only, skips non-vault dirs",
"correctness: VAULT_CONFIG caching — command parses vault-cli config list --output json at runtime detection (line 36), not cached at startup, so stale config is not an issue",
"tests: no test files changed — command is a markdown slash command, not Go code; precommit lint would catch any issues",
"security: VAULT_CONFIG JSON parsing — vault-cli config list --output json output is structured, subsequent commands use paths from config for ls/grep/find which are read-only operations"
]
}Replace broad `Bash(git:*)`, `Bash(gh:*)`, `Bash(dark-factory:*)` with the specific read-only subcommands the command actually invokes. - git: status, log, rev-parse, worktree, ls-remote, branch (all read-only) - gh: dropped — not actually used in the command body - dark-factory: narrowed to `dark-factory status:*` Aligns with sibling commands (sync-progress.md) and least-privilege principle. Addresses pr-reviewer MAJOR findings on PR #17.
superseded by new automated review
There was a problem hiding this comment.
The diff is between HEAD~1 (19a8b05) and HEAD (67a4bb6) — a security fix that narrows allowed-tools from broad patterns to specific read-only commands.
Review:
✅ Security fix is sound — git:* and gh:* and dark-factory:* were dangerously broad. Narrowing to git status, git log, git rev-parse, git worktree, git ls-remote, git branch, and dark-factory status (read-only operations) follows least-privilege correctly.
✅ No functional regression — the session-close command uses these tools for inspection only (git status, git log, git worktree list), not mutations. The narrow set covers all documented usages in the command.
✅ CHANGELOG entry exists — the ## Unreleased section already describes this command.
command -v was removed entirely — it was in the previous version's allowed-tools for runtime detection of gh, dark-factory, etc. The command uses command -v gh and command -v dark-factory at runtime (lines 41-44), but this tool is not in the allowed-tools list. This means those runtime checks would fail in a restricted environment. However, since the command degrades gracefully when tools are absent ("skip silently"), this is acceptable — the absence of command -v just means absent tools won't be detected, not that the command will error.
Conclusion: Approve. This is a pure security hardening with no functional impact.
{
"verdict": "approve",
"summary": "Security fix narrows allowed-tools from dangerously broad `git:*`, `gh:*`, `dark-factory:*` patterns to specific read-only subcommands. Correctly scoped to the actual inspection operations used in session-close (status, log, worktree list). No functional regression. CHANGELOG already updated.",
"comments": [],
"concerns_addressed": [
"security: allowed-tools narrowed from git:* to git status/log/rev-parse/worktree/ls-remote/branch — only read-only operations permitted"
]
}
Summary
Port
/session-closefrom~/.claude/commands/into the vault-cli plugin so coworkers installing vault-cli get the per-session lifecycle bookend (alongside the existing per-task and per-day trinities).Rationale
The vault docs ([[Task Lifecycle Guide]] + [[Daily Task Workflow Guide]]) document
work-on-task,sync-progress,complete-task,start-day,complete-day— all per-task and per-day vault-cli citizens. Per-session was the missing third lifecycle, living as an undocumented top-level command. This PR closes that gap.Changes
commands/session-close.md— new command, ported from the top-level version with config-driven paths (no hardcoded24 Tasks/,23 Goals/,50 Knowledge Base/,65 Runbooks/)README.md— add to plugin command tableCHANGELOG.md—## UnreleasedentryVault-Agnostic Pattern Compliance
Per [[Vault-Agnostic Slash Command Pattern]]:
vault-cli config list --output json(tasks_dir,goals_dir,daily_dir,knowledge_dir)^[0-9]+ [Rr]unbooks$regex (no config field today)gh,dark-factory,mcp__semantic-search__*,TaskList— all skip silently when absentVerification grep confirms no hardcoded references in the command body.
Test plan
/vault-cli:session-closein this same conversation/session-closebehavior (mode 1/2/3 outputs)gh; confirm Phase 3 git checks degrade gracefullyFollow-up (separate)
~/.claude/commands/session-close.mdafter this landsPersonal/CLAUDE.mdand~/.claude/CLAUDE.mdto reference/vault-cli:session-close