Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions .claude/hooks/install-deps.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,20 @@ set -euo pipefail
# Logs go to stderr because SessionStart hook stdout enters the model's context.
log() { echo "[install-deps] $*" >&2; }

# Explicit command callers print the selected Node bin:
# - --node-bin selects Node without installing.
# - --prepare-node-bin also runs dependency setup.
# Claude hook invocations omit the argument and keep stdout empty.
output_mode="${1:-}"
case "${output_mode}" in
""|--node-bin|--prepare-node-bin) ;;
*) log "unknown operation '${output_mode}'"; exit 2 ;;
esac
if (($# > 1)); then
log "expected at most one operation argument"
exit 2
fi

# A hook's non-interactive shell never sources .bashrc, so nvm is loaded here.
# - Without it, npm can resolve to a system Node (cloud images ship Node 22 at
# /opt/node22).
Expand All @@ -28,7 +42,7 @@ for dir in "${HOME}/.nvm" /root/.nvm; do
# version is not installed.
set +eu
# shellcheck disable=SC1091
. "${dir}/nvm.sh"
. "${dir}/nvm.sh" >&2
set -eu
break
fi
Expand All @@ -53,6 +67,7 @@ payload_cwd="$(read_payload_field cwd)"
checkout_lookup_dir="${payload_cwd:-${CLAUDE_PROJECT_DIR:-.}}"
checkout="$(git -C "${checkout_lookup_dir}" rev-parse --show-toplevel 2>/dev/null)" || {
log "no git checkout resolved from '${checkout_lookup_dir}' — skipping"
[[ -z "${output_mode}" ]] || exit 1
exit 0
}

Expand Down Expand Up @@ -138,7 +153,23 @@ persist_node_on_path() {
fi
}

persist_node_on_path
if [[ -n "${output_mode}" ]]; then
# Resolve inside the checkout: a version-manager shim can select another
# Node when invoked from the original shell's working directory.
physical_node_path="$(cd "${checkout}" && node -p process.execPath 2>/dev/null)" || {
log "could not resolve the selected Node in ${checkout}"
exit 1
}
if [[ "${physical_node_path}" != /* || ! -x "${physical_node_path}" ]]; then
log "selected Node in ${checkout} returned an invalid executable path"
exit 1
fi
node_bin="$(dirname "${physical_node_path}")"
printf '%s\n' "${node_bin}"
[[ "${output_mode}" != --node-bin ]] || exit 0
else
persist_node_on_path
fi
Comment thread
aliasunder marked this conversation as resolved.

# The stamp, marker, and lock (each described below) live in the checkout's
# git directory, which is per-worktree and never tracked. A .claude/ location
Expand Down
17 changes: 17 additions & 0 deletions .claude/hooks/prepare-command.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
# Repository command callers send the checkout and a non-SessionStart event
# as JSON on stdin. Only the selected physical Node bin goes to stdout.
set -euo pipefail

case "${1:-}" in
environment) installer_operation=--node-bin ;;
prepare) installer_operation=--prepare-node-bin ;;
*) echo "[prepare-command] expected environment or prepare" >&2; exit 2 ;;
esac
if (($# != 1)); then
echo "[prepare-command] expected exactly one operation argument" >&2
exit 2
fi

hook_directory="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
exec bash "${hook_directory}/install-deps.sh" "${installer_operation}"
Comment thread
aliasunder marked this conversation as resolved.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ CLAUDE.local.md
!.claude/hooks/
.claude/hooks/*
!.claude/hooks/install-deps.sh
!.claude/hooks/prepare-command.sh
TASKS.md
.DS_Store

Expand Down
11 changes: 8 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ on a folder for the full list.
server.json # MCP server registry manifest
render.yaml # Render Blueprint; stays at the repo root, the only place Render reads it
Dockerfile # Two-target build: local (default) + remote
.claude/ # Committed Claude Code hook setup only (the rest is gitignored): settings.json registers the hook below
hooks/install-deps.sh # Runs at session start and worktree entry: loads nvm, installs dependencies when needed (npm ci, sst install), and at session start puts the checkout's Node (the .nvmrc version when nvm has it, else nvm's default alias) first on PATH for the session's later Bash commands
.claude/ # Committed dependency hooks (the rest is gitignored); settings.json registers Claude Code setup
hooks/install-deps.sh # Claude session/worktree setup; loads the checkout's Node and installs dependencies/SST types when needed
hooks/prepare-command.sh # Optional command interface: environment selects Node; prepare also runs dependency setup
obsidian-headless/ # Lockfile-pinned obsidian-headless Sync CLI for the :remote image
rootfs/ # Container filesystem overlay for the :remote image: s6 init chain and services in etc/s6-overlay/, and usr/local/bin/get-sync-token, an in-container terminal sign-in to Obsidian that prints the Sync token for .env
templates/memory/ # About Me/ memory file templates for new vaults
Expand Down Expand Up @@ -416,6 +417,10 @@ change path separator behavior. Native Windows execution (without
Docker) is not a supported deployment and would break path handling
throughout the codebase.

## Dependency setup for agent commands

Before integrating agent command setup, read [Agent command setup](./CONTRIBUTING.md#agent-command-setup).

## Code style

<!-- distilled from vault Reference/code-standards-* on 2026-09-23; refresh: run the sync-code-standards skill -->
Expand Down Expand Up @@ -968,7 +973,7 @@ Two naming layers — MCP (JSON wire format) and TypeScript (internal):
`init-setup-vault.test.ts`, `check-sync-vault.test.ts` (the vault
check `init-setup-vault` runs), and `print-derived-env.test.ts` (the
derivation that `init-derive-env` publishes)
- the Claude Code hook in `.claude/hooks/` →
- the dependency hooks in `.claude/hooks/` →
`src/__tests__/install-deps.test.ts`
- the release scripts in `.github/scripts/` → `src/__tests__/`:
`generate-notes.test.ts`, `generate-cli-notes.test.ts`,
Expand Down
27 changes: 27 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,33 @@ npx @modelcontextprotocol/inspector

See the [README](./README.md#development) for full details on each mode.

## Agent command setup

Claude Code uses the committed session/worktree hook. Other clients can opt in
through `.claude/hooks/prepare-command.sh`:

| Operation | Behavior |
| ------------- | ------------------------------------------------- |
| `environment` | Select Node without writing install state |
| `prepare` | Select Node and wait for dependency and SST setup |

1. Invoke the entry point with the chosen operation. Send JSON with the
checkout's `cwd` and `hook_event_name: "PreToolUse"` on stdin.
2. Require a successful exit and stdout containing one nonempty absolute
directory with an executable `node`. Both operations print only the
selected physical Node bin directory.
3. Prepend the validated directory to the original command's PATH.

Ordinary `install-deps.sh` calls keep empty stdout and their existing behavior.
With nvm, selection uses the checkout's `.nvmrc` version when installed, then
nvm's default alias. If neither lookup finds an executable, or nvm is
unavailable, selection uses the current PATH. Setup never downloads Node.

Setup keeps the existing per-worktree lock and nonblocking failure behavior;
an exit of zero does not prove dependencies are ready. Check the actual test or
build result. Callers must allow time for setup and its lock wait. A checkout
without this entry point has not opted in.

## The `cli/` Package

`cli/` is a separate npm package (`npx vault-cortex@latest init`) that scaffolds the
Expand Down
1 change: 1 addition & 0 deletions scripts/__tests__/lint-shell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { describe, expect, it, onTestFinished } from "vitest"
/** Every shell script in the repo, sorted: what a run with no paths checks. */
const ALL_SHELL_SCRIPTS = [
".claude/hooks/install-deps.sh",
".claude/hooks/prepare-command.sh",
".github/scripts/generate-cli-notes.sh",
".github/scripts/generate-notes.sh",
".github/scripts/update-changelog.sh",
Expand Down
Loading
Loading