← Files NightshiftARCHIVED FILE
runtime/export-support.sh
10.9 KB · Oct 2, 2026 · 00:30 UTC
#!/usr/bin/env bash
# export-support.sh — write one local support bundle of allowlisted fields.
#
# Explicit owner action after Doctor. Never upload, transmit, attach, or open
# the file. Doctor itself stays read-only and must not invoke this.
# Known sensitive fields are omitted. This is not a complete sanitization.
#
# export-support.sh [--project DIR]
#
# Exit: 0 wrote the bundle · 1 usage · 2 refused
set -u
_here="${BASH_SOURCE[0]%/*}"; [ "$_here" != "${BASH_SOURCE[0]}" ] || _here=.
# shellcheck source=plugins/nightshift/lib/lib.sh
. "$_here/../lib/lib.sh"
PROJECT="${CLAUDE_PROJECT_DIR:-${CODEX_PROJECT_DIR:-$PWD}}"
while [ $# -gt 0 ]; do
case "$1" in
--project)
[ $# -ge 2 ] || { printf 'export-support: --project needs a value\n' >&2; exit 1; }
PROJECT="$2"
shift 2
;;
-h | --help)
awk 'NR == 1 { next } !/^#/ { exit } { sub(/^# ?/, ""); print }' "$0"
exit 1
;;
*) printf 'export-support: unknown argument: %s\n' "$1" >&2; exit 1 ;;
esac
done
HOST="$(cd -P "$PROJECT" 2>/dev/null && pwd)" || {
printf 'export-support: cannot cd to %s\n' "$PROJECT" >&2
exit 1
}
WORKSPACE="$HOST"
LINK_STATE="absent"
if [ -e "$HOST/.nightshift-link" ] || [ -L "$HOST/.nightshift-link" ]; then
if WORKSPACE="$(ns_workspace_root "$HOST" 2>/dev/null)"; then
LINK_STATE="valid"
else
printf 'export-support: invalid .nightshift-link — Nightshift will not guess a workspace\n' >&2
exit 2
fi
fi
NS="$WORKSPACE/.nightshift"
declare ARMED_FILE ENDED_FILE STOP_FILE SESSION_END_FILE PULSE_FILE SESSION_FILE WATCHMAN_FILE LEASE_FILE
ns_layout_set ARMED_FILE "$NS" armed
ns_layout_set ENDED_FILE "$NS" ended
ns_layout_set STOP_FILE "$NS" stop
ns_layout_set SESSION_END_FILE "$NS" session-end
ns_layout_set PULSE_FILE "$NS" pulse
ns_layout_set SESSION_FILE "$NS" session
ns_layout_set WATCHMAN_FILE "$NS" watchman
ns_layout_set LEASE_FILE "$NS" lease
[ -d "$NS" ] || {
printf 'export-support: no .nightshift/ at %s\n' "$WORKSPACE" >&2
exit 2
}
HOME_ROOT="${HOME:-}"
[ -n "$HOME_ROOT" ] && HOME_ROOT="$(cd -P "$HOME_ROOT" 2>/dev/null && pwd)" || HOME_ROOT=""
TARGET="$(ns_work_target "$WORKSPACE" 2>/dev/null || true)"
[ -n "$TARGET" ] && TARGET="$(cd -P "$TARGET" 2>/dev/null && pwd)" || TARGET=""
PLUGIN_JSON="$_here/../.claude-plugin/plugin.json"
PLUGIN_VER="unknown"
PLUGIN_NAME="nightshift"
if [ -f "$PLUGIN_JSON" ]; then
if command -v jq >/dev/null 2>&1; then
PLUGIN_VER="$(jq -r '.version // "unknown"' "$PLUGIN_JSON" 2>/dev/null || printf 'unknown')"
PLUGIN_NAME="$(jq -r '.name // "nightshift"' "$PLUGIN_JSON" 2>/dev/null || printf 'nightshift')"
else
PLUGIN_VER="$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$PLUGIN_JSON" | sed -n 1p)"
PLUGIN_VER="${PLUGIN_VER:-unknown}"
PLUGIN_NAME="$(sed -n 's/.*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$PLUGIN_JSON" | sed -n 1p)"
PLUGIN_NAME="${PLUGIN_NAME:-nightshift}"
fi
fi
STATE_KIND="$(ns_state_kind "$WORKSPACE")"
STATE_VER="$(ns_state_version "$WORKSPACE" || true)"
case "$STATE_KIND" in
current | legacy) ;;
*) STATE_VER="" ;;
esac
declare RULES
ns_layout_set RULES "$NS" rules
RULES_STATE="missing"
RULES_KEYS=""
if [ ! -f "$RULES" ]; then
RULES_STATE="missing"
elif ns_rules_load "$RULES"; then
RULES_STATE="valid"
RULES_KEYS="$(ns_rules_keys "$RULES" | tr '\n' ' ')"
else
RULES_STATE="unreadable"
fi
# The resolved view, never the policy files themselves. The four owner free-form rule
# patterns always ship as their length, whatever it is; any other setting's value is
# omitted the same way only past 80 characters. Sources and expiries always ship. The jq half
# sits next to this file, resolved without dirname so a hostile PATH cannot reach it.
NS_EXPORT_POLICY_JQ="$_here/export-policy.jq"
NS_EXPORT_POLICY_PY='
import json, sys
FREEFORM = {"forbiddenCommands", "protectedDirs", "neverCommitPatterns", "expectedEmail"}
d = json.load(sys.stdin)["settings"]
for k in sorted(d):
v = d[k]["value"]
text = v if isinstance(v, str) else json.dumps(v)
shown = text
if k in FREEFORM or len(text) > 80:
shown = "<redacted %d chars>" % len(text)
sys.stdout.write("%s=%s (%s, %s)\n" % (k, shown, d[k]["source"], d[k]["expiry"]))
'
POLICY_STATE="unreadable"
ns_policy_read_shift "$WORKSPACE" >/dev/null 2>&1
POLICY_READ_RC=$?
case "$POLICY_READ_RC" in
0) POLICY_STATE="valid" ;;
3) POLICY_STATE="absent" ;;
2) POLICY_STATE="malformed" ;;
*) POLICY_STATE="unreadable" ;;
esac
# Same redaction as the jq/python halves, from the already-resolved table — not a parser.
_ns_export_policy_table() {
local line name rest val meta
ns_policy_resolve_table "$1" | while IFS= read -r line; do
[ -n "$line" ] || continue
name="${line%%=*}"
rest="${line#*=}"
case "$rest" in
*' ('*)
meta="${rest##* (}"
val="${rest% ("$meta"}"
;;
*)
printf '%s\n' "$line"
continue
;;
esac
case "$name" in
forbiddenCommands | protectedDirs | neverCommitPatterns | expectedEmail)
printf '%s=<redacted %s chars> (%s\n' "$name" "${#val}" "$meta"
;;
*)
if [ "${#val}" -gt 80 ]; then
printf '%s=<redacted %s chars> (%s\n' "$name" "${#val}" "$meta"
else
printf '%s\n' "$line"
fi
;;
esac
done
}
POLICY_LINES=""
if POLICY_JSON="$(ns_policy_resolve "$WORKSPACE" 2>/dev/null)"; then
if command -v jq >/dev/null 2>&1; then
POLICY_LINES="$(printf '%s' "$POLICY_JSON" | jq -r -f "$NS_EXPORT_POLICY_JQ" 2>/dev/null)" || POLICY_LINES=""
elif command -v python3 >/dev/null 2>&1; then
POLICY_LINES="$(printf '%s' "$POLICY_JSON" | python3 -c "$NS_EXPORT_POLICY_PY" 2>/dev/null)" || POLICY_LINES=""
else
POLICY_LINES="$(_ns_export_policy_table "$WORKSPACE" 2>/dev/null)" || POLICY_LINES=""
fi
fi
REASON="$(ns_reason_code "$NS")"
REASON_LABEL=""
[ -z "$REASON" ] || REASON_LABEL="$(ns_reason_label "$REASON")"
LEASE_STATE="absent"
LEASE_HOST=""
LEASE_GENERATION=""
LEASE_MODE=""
if [ -e "$LEASE_FILE" ] || [ -L "$LEASE_FILE" ]; then
if ns_lease_valid "$NS"; then
LEASE_STATE="valid"
LEASE_HOST="$NS_LEASE_HOST"
LEASE_GENERATION="$NS_LEASE_GENERATION"
if [ -n "$NS_LEASE_NONCE" ]; then
LEASE_MODE="recovered"
else
LEASE_MODE="interactive"
fi
else
LEASE_STATE="malformed"
fi
fi
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
declare outdir
ns_layout_set outdir "$NS" support
mkdir -p "$outdir" || {
printf 'export-support: cannot create %s\n' "$outdir" >&2
exit 2
}
tmp="$outdir/.$stamp.$$"
dest="$outdir/${stamp}.txt"
{
printf 'Nightshift support bundle\n'
printf 'Generated: %s\n' "$stamp"
printf '\n== plugin ==\n'
printf 'name: %s\n' "$PLUGIN_NAME"
printf 'version: %s\n' "$PLUGIN_VER"
printf '\n== host ==\n'
printf 'uname: %s\n' "$(uname -s)"
printf 'link: %s\n' "$LINK_STATE"
printf '\n== state ==\n'
printf 'kind: %s\n' "$STATE_KIND"
[ -z "$STATE_VER" ] || printf 'version: %s\n' "$STATE_VER"
printf '\n== identities ==\n'
if id_host="$(ns_tokenize_text "$HOST" "$HOME_ROOT" "$WORKSPACE" "$TARGET")"; then
printf 'task: %s\n' "$id_host"
else
printf 'task: omitted\n'
fi
if id_ws="$(ns_tokenize_text "$WORKSPACE" "$HOME_ROOT" "$WORKSPACE" "$TARGET")"; then
printf 'workspace: %s\n' "$id_ws"
else
printf 'workspace: omitted\n'
fi
if [ -n "$TARGET" ]; then
if id_tg="$(ns_tokenize_text "$TARGET" "$HOME_ROOT" "$WORKSPACE" "$TARGET")"; then
printf 'work_target: %s\n' "$id_tg"
else
printf 'work_target: omitted\n'
fi
else
printf 'work_target: unresolved\n'
fi
printf '\n== markers ==\n'
if [ -L "$ARMED_FILE" ]; then
printf 'armed: unusable\n'
else
printf 'armed: %s\n' "$( [ -f "$ARMED_FILE" ] && printf yes || printf no )"
fi
if [ -L "$ENDED_FILE" ]; then
printf 'ended: unusable\n'
else
printf 'ended: %s\n' "$( [ -f "$ENDED_FILE" ] && printf yes || printf no )"
fi
printf 'stop: %s\n' "$( [ -f "$STOP_FILE" ] && printf yes || printf no )"
if [ -L "$SESSION_END_FILE" ]; then
printf 'session_end: unusable\n'
else
printf 'session_end: %s\n' "$( [ -f "$SESSION_END_FILE" ] && printf yes || printf no )"
fi
if [ -L "$PULSE_FILE" ]; then
printf 'shift_pulse: unusable\n'
else
printf 'shift_pulse: %s\n' "$( [ -f "$PULSE_FILE" ] && printf yes || printf no )"
fi
if [ -L "$SESSION_FILE" ]; then
printf 'session_record: unusable\n'
else
printf 'session_record: %s\n' "$( [ -f "$SESSION_FILE" ] && printf present || printf absent )"
fi
printf 'process_lease: %s\n' "$LEASE_STATE"
[ -z "$LEASE_HOST" ] || printf 'lease_host: %s\n' "$LEASE_HOST"
[ -z "$LEASE_GENERATION" ] || printf 'lease_generation: %s\n' "$LEASE_GENERATION"
[ -z "$LEASE_MODE" ] || printf 'lease_mode: %s\n' "$LEASE_MODE"
if [ -L "$WATCHMAN_FILE" ]; then
printf 'watchman_pidfile: unusable\n'
else
printf 'watchman_pidfile: %s\n' "$( [ -f "$WATCHMAN_FILE" ] && printf present || printf absent )"
fi
printf '\n== rules ==\n'
printf 'validity: %s\n' "$RULES_STATE"
printf 'keys: %s\n' "${RULES_KEYS:-}"
printf '\n== resolved policy ==\n'
printf 'shift_policy: %s\n' "$POLICY_STATE"
if [ -n "$POLICY_LINES" ]; then
printf '%s\n' "$POLICY_LINES"
fi
printf '\n== evidence summary ==\n'
printf '%s\n' "$(ns_evidence_counts "$WORKSPACE")"
printf 'liveness: %s\n' "$(ns_status_liveness "$NS" "$(rule "$WORKSPACE" watchMinutes "${NIGHTSHIFT_WATCH:-}")")"
activity="$(ns_status_last_activity "$NS")"
printf 'last activity: %s\n' "${activity:-none}"
printf 'last checkpoint: %s\n' "$(ns_status_last_checkpoint "$WORKSPACE")"
printf 'stall attempts: %s\n' "$(ns_status_stall_attempts "$NS")"
declare inv
ns_layout_set inv "$NS" capabilities
if [ -f "$inv" ] && [ ! -L "$inv" ] && command -v jq >/dev/null 2>&1; then
printf 'inventory items: %s\n' "$(jq '.items | length' "$inv" 2>/dev/null || printf 0)"
else
printf 'inventory items: omitted\n'
fi
printf '\n== runtime log ==\n'
printf 'omitted\n'
printf '\n== watchman reason ==\n'
if [ -n "$REASON" ]; then
printf 'code: %s\n' "$REASON"
printf 'label: %s\n' "$REASON_LABEL"
else
printf 'code: none\n'
fi
} >"$tmp" || {
rm -f "$tmp"
printf 'export-support: failed to write bundle\n' >&2
exit 2
}
chmod 600 "$tmp" || {
rm -f "$tmp"
printf 'export-support: failed to restrict bundle mode\n' >&2
exit 2
}
mv "$tmp" "$dest" || {
rm -f "$tmp"
printf 'export-support: failed to publish bundle\n' >&2
exit 2
}
printf 'Support bundle: %s\n' "$dest"
printf 'Included: plugin version, markers, the resolved policy view, counts\n'
printf 'Omitted: scheduled.log, evidence ledger raw output, known sensitive fields, repository contents, diffs, transcripts, prompts, owner files, credentials, network, session identities, lease capabilities, capability inventory contents, policy files\n'
printf 'Known sensitive fields are omitted. Inspect the file before sharing. Never uploaded, attached, or opened automatically.\n'
exit 0
SHA-256: 3a7192a173235f85d4e24fd3e1a711dd55b908af63273e0bb0eda32e3f757caf