diff --git a/.mise/tasks/audit b/.mise/tasks/audit
index 21f532e..1c01fd7 100755
--- a/.mise/tasks/audit
+++ b/.mise/tasks/audit
@@ -9,6 +9,7 @@
from __future__ import annotations
import os
+import subprocess
import sys
from pathlib import Path
@@ -25,6 +26,13 @@ def main() -> int:
print(f"Error: notes directory not found: {notes_dir}", file=sys.stderr)
return 1
+ readable = subprocess.run(
+ [str(repo_dir / "lib" / "readable-state.sh"), "require", str(notes_dir)],
+ check=False,
+ )
+ if readable.returncode != 0:
+ return readable.returncode
+
try:
top_n = parse_top(os.environ.get("usage_top", "10"))
except ValueError as exc:
diff --git a/.mise/tasks/commit b/.mise/tasks/commit
index a6aef02..711a9d0 100755
--- a/.mise/tasks/commit
+++ b/.mise/tasks/commit
@@ -24,6 +24,7 @@ if [ ! -f "$manifest" ]; then
echo "No manifest found. Run 'notes obfuscate' first." >&2
exit 1
fi
+require_readable_notes_state "$abs_notes_dir"
ARGS=()
if [ -n "${usage_files:-}" ]; then
diff --git a/.mise/tasks/conflicts b/.mise/tasks/conflicts
index 9183c2f..9cd8125 100755
--- a/.mise/tasks/conflicts
+++ b/.mise/tasks/conflicts
@@ -11,6 +11,7 @@ require_git
notes_dir="${usage_dir:-notes}"
out_arg="${usage_out:-}"
repo_root=$(git -C "$TARGET_DIR" rev-parse --show-toplevel)
+require_readable_notes_state "$repo_root/$notes_dir"
if ! records=$(notes_conflict_records "$repo_root" "$notes_dir"); then
exit 1
diff --git a/.mise/tasks/diff b/.mise/tasks/diff
index e0f9694..d6e1a5d 100755
--- a/.mise/tasks/diff
+++ b/.mise/tasks/diff
@@ -16,6 +16,7 @@ pr_number="${usage_pr:-}"
out_arg="${usage_out:-}"
abs_notes_dir="$TARGET_DIR/$notes_dir"
repo_root=$(git -C "$TARGET_DIR" rev-parse --show-toplevel)
+require_readable_notes_state "$abs_notes_dir"
ARGS=()
if [ -n "${usage_refs:-}" ]; then
diff --git a/.mise/tasks/list b/.mise/tasks/list
index becae19..ff940b9 100755
--- a/.mise/tasks/list
+++ b/.mise/tasks/list
@@ -9,6 +9,7 @@
set -euo pipefail
REPO_DIR="${MISE_CONFIG_ROOT:?MISE_CONFIG_ROOT not set}"
+source "$REPO_DIR/lib/readable-state.sh"
TARGET_DIR="${NOTES_CALLER_PWD:-.}"
notes_dir="$TARGET_DIR/${usage_dir:-notes}"
@@ -16,6 +17,7 @@ if [ ! -d "$notes_dir" ]; then
echo "Error: notes directory not found: $notes_dir" >&2
exit 1
fi
+require_readable_notes_state "$notes_dir"
python3 - "$REPO_DIR" "$notes_dir" "${usage_json:-false}" "${usage_recent:-}" "${usage_tag:-}" "${usage_type:-}" "${usage_status:-}" <<'PY'
import json
diff --git a/.mise/tasks/merge b/.mise/tasks/merge
index 6335a35..abb1aea 100755
--- a/.mise/tasks/merge
+++ b/.mise/tasks/merge
@@ -14,7 +14,8 @@ if [ "${usage_dry_run:-false}" != "true" ]; then
exit 1
fi
-args=(conflicts --dir "${usage_dir:-notes}")
+notes_dir="${usage_dir:-notes}"
+args=(conflicts --dir "$notes_dir")
if [ -n "${usage_out:-}" ]; then
args+=(--out "${usage_out}")
fi
diff --git a/.mise/tasks/new b/.mise/tasks/new
index 015ab72..35e5182 100755
--- a/.mise/tasks/new
+++ b/.mise/tasks/new
@@ -10,6 +10,8 @@
#USAGE flag "--dir
" default="notes" help="Notes directory relative to repo root"
set -euo pipefail
+REPO_DIR="${MISE_CONFIG_ROOT:?MISE_CONFIG_ROOT not set}"
+source "$REPO_DIR/lib/readable-state.sh"
TARGET_DIR="${NOTES_CALLER_PWD:-.}"
notes_dir="$TARGET_DIR/${usage_dir:-notes}"
today=$(date +%Y-%m-%d)
@@ -18,6 +20,7 @@ if [ ! -d "$notes_dir" ]; then
echo "Error: notes directory not found: $notes_dir" >&2
exit 1
fi
+require_readable_notes_state "$notes_dir"
file="$notes_dir/${usage_slug}.md"
if [ -f "$file" ]; then
diff --git a/.mise/tasks/parse b/.mise/tasks/parse
index 8e02fd0..b800395 100755
--- a/.mise/tasks/parse
+++ b/.mise/tasks/parse
@@ -9,6 +9,7 @@ from __future__ import annotations
import json
import os
+import subprocess
import sys
from pathlib import Path
@@ -45,6 +46,13 @@ def existing_path_candidates(selector: str) -> list[Path]:
def main() -> int:
+ readable = subprocess.run(
+ [str(repo_dir / "lib" / "readable-state.sh"), "require", str(notes_dir)],
+ check=False,
+ )
+ if readable.returncode != 0:
+ return readable.returncode
+
note_path = None
for candidate in existing_path_candidates(selector):
if candidate.is_file():
diff --git a/.mise/tasks/pull b/.mise/tasks/pull
index 72332a7..c3e4475 100755
--- a/.mise/tasks/pull
+++ b/.mise/tasks/pull
@@ -16,12 +16,16 @@ abs_notes_dir="$(cd "$TARGET_DIR/$notes_dir" 2>/dev/null && pwd)" || {
exit 1
}
manifest="$abs_notes_dir/.manifest"
+readable_state=$(notes_readable_state "$abs_notes_dir")
+if [ "$readable_state" = "locked" ]; then
+ echo "Notes are locked; skipping readable manifest checks and reconciliation."
+fi
# --- Check for assume-unchanged manifest ---
# git pull can fail when notes/.manifest is assume-unchanged but differs
# between local HEAD and upstream. Detect this early and repair.
-if [ -f "$manifest" ]; then
+if [ "$readable_state" = "readable" ] && [ -f "$manifest" ]; then
manifest_rc=0
NOTES_QUIET_MANIFEST_CHECK=1 detect_assume_unchanged_manifest "$notes_dir" || manifest_rc=$?
@@ -48,8 +52,9 @@ fi
# --- Determine obfuscation state ---
# We need to know if we're deobfuscated so we can re-apply suppression after pull.
-obf_status="unknown"
-if [ -f "$manifest" ]; then
+obf_status="$readable_state"
+if [ "$readable_state" = "readable" ] && [ -f "$manifest" ]; then
+ obf_status="unknown"
first_name=$(head -1 "$manifest" | cut -f2)
first_hash=$(head -1 "$manifest" | cut -f1)
if [ -n "$first_name" ] && [ -f "$abs_notes_dir/$first_name" ]; then
diff --git a/.mise/tasks/search b/.mise/tasks/search
index eb4b2ac..93cdc3b 100755
--- a/.mise/tasks/search
+++ b/.mise/tasks/search
@@ -10,6 +10,7 @@
set -euo pipefail
REPO_DIR="${MISE_CONFIG_ROOT:?MISE_CONFIG_ROOT not set}"
+source "$REPO_DIR/lib/readable-state.sh"
TARGET_DIR="${NOTES_CALLER_PWD:-.}"
notes_dir="$TARGET_DIR/${usage_dir:-notes}"
@@ -17,6 +18,7 @@ if [ ! -d "$notes_dir" ]; then
echo "Error: notes directory not found: $notes_dir" >&2
exit 1
fi
+require_readable_notes_state "$notes_dir"
python3 - "$REPO_DIR" "$notes_dir" "${usage_query:-}" "${usage_json:-false}" "${usage_tag:-}" "${usage_type:-}" "${usage_status:-}" "${usage_limit:-50}" <<'PY'
import json
diff --git a/.mise/tasks/show b/.mise/tasks/show
index a50401c..f0812de 100755
--- a/.mise/tasks/show
+++ b/.mise/tasks/show
@@ -6,6 +6,7 @@
set -euo pipefail
REPO_DIR="${MISE_CONFIG_ROOT:?MISE_CONFIG_ROOT not set}"
+source "$REPO_DIR/lib/readable-state.sh"
TARGET_DIR="${NOTES_CALLER_PWD:-.}"
notes_dir="$TARGET_DIR/${usage_dir:-notes}"
@@ -13,6 +14,7 @@ if [ ! -d "$notes_dir" ]; then
echo "Error: notes directory not found: $notes_dir" >&2
exit 1
fi
+require_readable_notes_state "$notes_dir"
python3 - "$REPO_DIR" "$TARGET_DIR" "$notes_dir" "${usage_note:-}" "${usage_json:-false}" <<'PY'
import json
diff --git a/README.md b/README.md
index fca40a0..0f65267 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
**Collective memory, encrypted.**
-[](test/)
+[](test/)

[](LICENSE)
diff --git a/lib/changes.sh b/lib/changes.sh
index 92b34ed..9295129 100644
--- a/lib/changes.sh
+++ b/lib/changes.sh
@@ -243,6 +243,7 @@ detect_changes() {
resolve_notes_dir "$abs_notes_dir" || return
local repo_root="$RESOLVED_REPO_ROOT"
local notes_dir="$RESOLVED_NOTES_DIR"
+ require_readable_notes_state "$abs_notes_dir" || return
local workspace rc=0
workspace=$(mktemp -d) || return
: > "$workspace/detected"
diff --git a/lib/common.sh b/lib/common.sh
index f059a72..737fb0d 100644
--- a/lib/common.sh
+++ b/lib/common.sh
@@ -15,6 +15,7 @@ TARGET_DIR="${NOTES_CALLER_PWD:-.}"
NOTES_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
NOTES_REPO_DIR="$(cd "$NOTES_LIB_DIR/.." && pwd)"
HOOKS_DIR="$NOTES_REPO_DIR/hooks"
+source "$NOTES_LIB_DIR/readable-state.sh"
# ── Require checks ────────────────────────────────────────────
diff --git a/lib/obfuscate.sh b/lib/obfuscate.sh
index d64a5bc..5a11ad8 100644
--- a/lib/obfuscate.sh
+++ b/lib/obfuscate.sh
@@ -104,6 +104,7 @@ build_obfuscation_plan() {
local manifest="$notes_dir/.manifest"
local workspace candidates manifest_input rc=0
+ require_readable_notes_state "$notes_dir" || return
workspace=$(mktemp -d) || {
echo "Error: failed to create obfuscation workspace" >&2
return 1
@@ -158,6 +159,7 @@ apply_obfuscation_plan() {
local manifest="$notes_dir/.manifest"
local new_entries kind id relpath new_count=0
+ require_readable_notes_state "$notes_dir" || return
[ -s "$plan_file" ] || return 2
new_entries=$(mktemp) || {
@@ -511,6 +513,7 @@ rename_to_readable() {
local manifest="$notes_dir/.manifest"
local count=0 dirty_count=0
+ require_readable_notes_state "$notes_dir" || return
[ ! -f "$manifest" ] && return 1
# _rename_one_to_readable returns: 0=renamed, 2=skipped,
diff --git a/lib/readable-state.sh b/lib/readable-state.sh
new file mode 100755
index 0000000..0908028
--- /dev/null
+++ b/lib/readable-state.sh
@@ -0,0 +1,100 @@
+#!/usr/bin/env bash
+# readable-state.sh — classify whether managed note content is readable
+
+_GIT_CRYPT_HEADER_HEX="00474954435259505400"
+
+# Print "readable" or "locked" for a notes directory.
+# Missing/non-Git/plaintext note directories remain readable so their owning
+# commands can preserve their existing validation and initialization behavior.
+notes_readable_state() {
+ local notes_dir="${1:?usage: notes_readable_state }"
+ local abs_notes_dir repo_root rel_notes_dir manifest attr filter header
+
+ if [ ! -d "$notes_dir" ]; then
+ printf '%s\n' "readable"
+ return 0
+ fi
+ if ! abs_notes_dir=$(cd "$notes_dir" 2>/dev/null && pwd -P); then
+ echo "Error: failed to resolve notes directory: $notes_dir" >&2
+ return 2
+ fi
+ if ! repo_root=$(git -C "$abs_notes_dir" rev-parse --show-toplevel 2>/dev/null); then
+ printf '%s\n' "readable"
+ return 0
+ fi
+
+ case "$abs_notes_dir" in
+ "$repo_root"/*) rel_notes_dir=${abs_notes_dir#"$repo_root"/} ;;
+ *)
+ echo "Error: notes directory is outside its Git repository: $abs_notes_dir" >&2
+ return 2
+ ;;
+ esac
+
+ manifest="$abs_notes_dir/.manifest"
+ if [ ! -f "$manifest" ]; then
+ printf '%s\n' "readable"
+ return 0
+ fi
+
+ if ! attr=$(git -C "$repo_root" check-attr filter -- "$rel_notes_dir/.manifest"); then
+ echo "Error: failed to inspect encryption attributes for $rel_notes_dir/.manifest" >&2
+ return 2
+ fi
+ filter=${attr##*: }
+ if [ "$filter" != "git-crypt" ]; then
+ printf '%s\n' "readable"
+ return 0
+ fi
+
+ if ! header=$(LC_ALL=C od -An -tx1 -N10 "$manifest" 2>/dev/null); then
+ echo "Error: failed to inspect encrypted manifest header: $manifest" >&2
+ return 2
+ fi
+ header=$(printf '%s' "$header" | tr -d '[:space:]')
+
+ if [ "$header" = "$_GIT_CRYPT_HEADER_HEX" ]; then
+ printf '%s\n' "locked"
+ else
+ printf '%s\n' "readable"
+ fi
+}
+
+require_readable_notes_state() {
+ local notes_dir="${1:?usage: require_readable_notes_state }"
+ local state status=0
+
+ state=$(notes_readable_state "$notes_dir") || status=$?
+ if [ "$status" -ne 0 ]; then
+ return "$status"
+ fi
+ if [ "$state" = "locked" ]; then
+ echo "Error: git-crypt is locked. Run 'notes unlock' first." >&2
+ return 1
+ fi
+}
+
+_readable_state_main() {
+ local command="${1:-}"
+ local notes_dir="${2:-}"
+
+ case "$command" in
+ probe)
+ [ -n "$notes_dir" ] || { echo "usage: readable-state.sh probe " >&2; return 64; }
+ notes_readable_state "$notes_dir"
+ ;;
+ require)
+ [ -n "$notes_dir" ] || { echo "usage: readable-state.sh require " >&2; return 64; }
+ require_readable_notes_state "$notes_dir"
+ ;;
+ *)
+ echo "usage: readable-state.sh " >&2
+ return 64
+ ;;
+ esac
+}
+
+if [ "${BASH_SOURCE[0]}" = "$0" ]; then
+ set -uo pipefail
+ _readable_state_main "$@"
+fi
diff --git a/lib/suppress.sh b/lib/suppress.sh
index 8b39b54..2ea40b1 100644
--- a/lib/suppress.sh
+++ b/lib/suppress.sh
@@ -232,6 +232,7 @@ set_status_suppression() {
local scoped_ids=("$@")
local manifest="$abs_notes_dir/.manifest"
[ ! -f "$manifest" ] && return
+ require_readable_notes_state "$abs_notes_dir" || return
resolve_notes_dir "$abs_notes_dir" || return
local repo_root="$RESOLVED_REPO_ROOT"
@@ -265,6 +266,7 @@ clear_status_suppression() {
local scoped_ids=("$@")
local manifest="$abs_notes_dir/.manifest"
[ ! -f "$manifest" ] && return
+ require_readable_notes_state "$abs_notes_dir" || return
resolve_notes_dir "$abs_notes_dir" || return
local repo_root="$RESOLVED_REPO_ROOT"
@@ -346,6 +348,7 @@ detect_stale_readable_notes() {
local abs_notes_dir="${1:?usage: detect_stale_readable_notes }"
local manifest="$abs_notes_dir/.manifest"
[ -f "$manifest" ] || return 0
+ require_readable_notes_state "$abs_notes_dir" || return
resolve_notes_dir "$abs_notes_dir" || return
local repo_root="$RESOLVED_REPO_ROOT"
@@ -495,6 +498,7 @@ rebuild_status_suppression() {
local abs_notes_dir="${1:?usage: rebuild_status_suppression }"
local manifest="$abs_notes_dir/.manifest"
[ -f "$manifest" ] || return 0
+ require_readable_notes_state "$abs_notes_dir" || return
resolve_notes_dir "$abs_notes_dir" || return
local repo_root="$RESOLVED_REPO_ROOT"
diff --git a/test/changes.bats b/test/changes.bats
index 22e98bd..e8808d6 100644
--- a/test/changes.bats
+++ b/test/changes.bats
@@ -44,6 +44,34 @@ SH
# ── detect_changes ────────────────────────────────────────────
+@test "notes changes refuses locked encrypted content before classifying paths" {
+ printf 'notes/** filter=git-crypt diff=git-crypt\n' > "$NOTES_CALLER_PWD/.gitattributes"
+ printf '\0GITCRYPT\0encrypted manifest bytes\n' > "$MANIFEST"
+ printf '\0GITCRYPT\0encrypted note bytes\n' > "$NOTES_CALLER_PWD/notes/aaaaaaaa"
+
+ run notes changes --summary
+ [ "$status" -ne 0 ]
+ [[ "$output" == *"git-crypt is locked"* ]]
+ [[ "$output" == *"notes unlock"* ]]
+ [[ "$output" != *"aaaaaaaa"* ]]
+
+ # A keyless clone can report initialized=false even though the encrypted
+ # manifest is present and unreadable.
+ run notes changes
+ [ "$status" -ne 0 ]
+ [[ "$output" == *"git-crypt is locked"* ]]
+ [[ "$output" != *"aaaaaaaa"* ]]
+}
+
+@test "notes changes preserves clean output when encryption is unlocked" {
+ printf 'notes/** filter=git-crypt diff=git-crypt\n' > "$NOTES_CALLER_PWD/.gitattributes"
+
+ run notes changes --summary
+
+ [ "$status" -eq 0 ]
+ [ "$output" = "No changes." ]
+}
+
@test "detect_changes: no changes when files match HEAD" {
run detect_changes "$NOTES_CALLER_PWD/notes"
[ "$status" -eq 0 ]
diff --git a/test/readable-state.bats b/test/readable-state.bats
new file mode 100644
index 0000000..2fdabbd
--- /dev/null
+++ b/test/readable-state.bats
@@ -0,0 +1,131 @@
+#!/usr/bin/env bats
+
+load test_helper
+source "$REPO_DIR/lib/changes.sh"
+
+setup() {
+ export NOTES_CALLER_PWD="$BATS_TEST_TMPDIR/locked-repo"
+ git -C "$BATS_TEST_TMPDIR" init -q -b main locked-repo
+ git -C "$NOTES_CALLER_PWD" config user.name "Notes tests"
+ git -C "$NOTES_CALLER_PWD" config user.email "notes-tests@example.invalid"
+ git -C "$NOTES_CALLER_PWD" config filter.git-crypt.clean cat
+ git -C "$NOTES_CALLER_PWD" config filter.git-crypt.smudge cat
+ mkdir -p "$NOTES_CALLER_PWD/notes"
+ printf 'notes/** filter=git-crypt diff=git-crypt\n' > "$NOTES_CALLER_PWD/.gitattributes"
+ printf '\0GITCRYPT\0encrypted manifest bytes\n' > "$NOTES_CALLER_PWD/notes/.manifest"
+ printf '\0GITCRYPT\0encrypted note bytes\n' > "$NOTES_CALLER_PWD/notes/aaaaaaaa"
+ git -C "$NOTES_CALLER_PWD" add .gitattributes notes/.manifest notes/aaaaaaaa
+ git -C "$NOTES_CALLER_PWD" commit -q -m "locked fixture"
+}
+
+assert_locked_failure() {
+ [ "$status" -ne 0 ]
+ [[ "$output" == *"git-crypt is locked"* ]]
+ [[ "$output" == *"notes unlock"* ]]
+ [[ "$output" != *"aaaaaaaa"* ]]
+}
+
+@test "readable-state classifies encrypted and plaintext managed manifests" {
+ run "$REPO_DIR/lib/readable-state.sh" probe "$NOTES_CALLER_PWD/notes"
+ [ "$status" -eq 0 ]
+ [ "$output" = "locked" ]
+
+ printf 'aaaaaaaa\talpha.md\n' > "$NOTES_CALLER_PWD/notes/.manifest"
+ run "$REPO_DIR/lib/readable-state.sh" probe "$NOTES_CALLER_PWD/notes"
+ [ "$status" -eq 0 ]
+ [ "$output" = "readable" ]
+
+ printf '\0GITCRYPT\0encrypted manifest bytes\n' > "$NOTES_CALLER_PWD/notes/.manifest"
+ printf 'notes/** -filter\n' > "$NOTES_CALLER_PWD/.gitattributes"
+ run "$REPO_DIR/lib/readable-state.sh" probe "$NOTES_CALLER_PWD/notes"
+ [ "$status" -eq 0 ]
+ [ "$output" = "readable" ]
+}
+
+@test "readable-state fails closed when an encrypted manifest cannot be inspected" {
+ local failing_bin="$BATS_TEST_TMPDIR/failing-bin"
+ mkdir -p "$failing_bin"
+ cat > "$failing_bin/od" <<'SH'
+#!/usr/bin/env bash
+exit 73
+SH
+ chmod +x "$failing_bin/od"
+
+ PATH="$failing_bin:$PATH" run "$REPO_DIR/lib/readable-state.sh" probe "$NOTES_CALLER_PWD/notes"
+ [ "$status" -eq 2 ]
+ [[ "$output" == *"failed to inspect encrypted manifest header"* ]]
+}
+
+@test "shared readable-state boundaries cannot be bypassed by direct consumers" {
+ local plan="$BATS_TEST_TMPDIR/obfuscation-plan"
+
+ run detect_changes "$NOTES_CALLER_PWD/notes"
+ assert_locked_failure
+
+ run build_obfuscation_plan "$NOTES_CALLER_PWD/notes" "$plan"
+ assert_locked_failure
+ [ ! -e "$plan" ]
+
+ run rename_to_readable "$NOTES_CALLER_PWD/notes"
+ assert_locked_failure
+
+ run rebuild_status_suppression "$NOTES_CALLER_PWD/notes"
+ assert_locked_failure
+}
+
+@test "read-only note consumers refuse locked content without exposing opaque paths" {
+ while IFS= read -r invocation; do
+ [ -n "$invocation" ] || continue
+ # shellcheck disable=SC2086
+ run notes $invocation
+ assert_locked_failure
+ done <<'EOF'
+changes --summary
+diff
+list --json
+search orientation --json
+audit --json
+show aaaaaaaa
+parse notes/aaaaaaaa
+conflicts
+merge --dry-run
+EOF
+}
+
+@test "mutating note consumers refuse locked content without changing index or worktree" {
+ local before_status before_index invocation
+ before_status=$(git -C "$NOTES_CALLER_PWD" status --porcelain=v1 -uall)
+ before_index=$(git -C "$NOTES_CALLER_PWD" write-tree)
+
+ while IFS= read -r invocation; do
+ [ -n "$invocation" ] || continue
+ # shellcheck disable=SC2086
+ run notes $invocation
+ assert_locked_failure
+ [ "$(git -C "$NOTES_CALLER_PWD" status --porcelain=v1 -uall)" = "$before_status" ]
+ [ "$(git -C "$NOTES_CALLER_PWD" write-tree)" = "$before_index" ]
+ done <<'EOF'
+stage --all
+commit --all -m locked
+new --slug locked --title Locked
+obfuscate
+deobfuscate
+suppress-refresh
+EOF
+
+ [ ! -e "$NOTES_CALLER_PWD/notes/locked.md" ]
+}
+
+@test "pull remains available while locked and skips readable reconciliation" {
+ local remote="$BATS_TEST_TMPDIR/remote.git"
+ git init -q --bare "$remote"
+ git -C "$NOTES_CALLER_PWD" remote add origin "$remote"
+ git -C "$NOTES_CALLER_PWD" push -q -u origin main
+
+ run notes pull
+
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"Notes are locked; skipping readable manifest checks and reconciliation."* ]]
+ [[ "$output" == *"Pull complete."* ]]
+ [ -z "$(git -C "$NOTES_CALLER_PWD" status --porcelain=v1 -uall)" ]
+}