Skip to content

Instantly share code, notes, and snippets.

@ivan
Last active May 3, 2026 11:42
Show Gist options
  • Select an option

  • Save ivan/5d5b0f26149239c6350cf77ea936b2a5 to your computer and use it in GitHub Desktop.

Select an option

Save ivan/5d5b0f26149239c6350cf77ea936b2a5 to your computer and use it in GitHub Desktop.
Maybe a proper zsh history logger, via good requirements + ChatGPT 5.2 Thinking + 5.5 Pro + 5.5 Thinking
# This is slop authored by ChatGPT 5.5 Pro on 2026-05-03, using some earlier
# inputs from ChatGPT 5.2 Thinking.
# zsh JSONL history archive
# Install: source this near the end of ~/.zshrc.
#
# Records go to one unique JSONL file per interactive zsh session:
# ${ZSH_HISTORY_ARCHIVE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/zsh-history-archive}/hist.<session>.jsonl
#
# Record types:
# session_start
# command_start before zsh executes the entered command line
# command_end after zsh returns from evaluating the entered command line
# async_start after zsh has registered a newly-created background job
# async_end when a tracked background job reaches zsh job-state "done"
# async_lost if a tracked background job disappears from zsh's job table
# session_end
#
# Intentional design choices:
# - JSON Lines, one object per line.
# - One file per zsh process: no locking and no cross-shell append interleaving.
# - Background completion tracking uses zsh's job table, not PID guessing.
# - This block unsets NOTIFY so the CHLD trap can see completed jobs before zsh
# prints/deletes them. Install this near the end of ~/.zshrc and do not setopt
# NOTIFY afterwards if you want reliable async_end records.
#
# XXX: zsh does not expose an exact, non-consuming "wait status" for background
# pipelines. For async_end, status is the parsed status of the last process in
# the job; the full per-process state array is also logged. Simple "cmd &" jobs
# get the exact command exit status.
#
# XXX: list-form pre-existing CHLD traps are not safely chainable here. If one is
# present when this file is sourced, background completion tracking is disabled
# rather than clobbering that trap. Function-form TRAPCHLD is chained.
if [[ -o interactive && ${ZSH_SUBSHELL:-0} == 0 ]]; then
setopt EXTENDED_HISTORY
autoload -Uz add-zsh-hook 2>/dev/null || true
# Remove old hooks if this file is re-sourced.
add-zsh-hook -d preexec __zhistarchive_preexec 2>/dev/null || true
add-zsh-hook -d precmd __zhistarchive_precmd 2>/dev/null || true
add-zsh-hook -d zshexit __zhistarchive_zshexit 2>/dev/null || true
# Close old fd if this file is re-sourced.
if (( ${+__zhistarchive_fd} && __zhistarchive_fd >= 0 )); then
exec {__zhistarchive_fd}>&- 2>/dev/null || true
fi
zmodload zsh/datetime 2>/dev/null || true
zmodload zsh/system 2>/dev/null || true
zmodload zsh/parameter 2>/dev/null || true
typeset -gi __zhistarchive_enabled=0
typeset -gi __zhistarchive_have_syswrite=0
typeset -gi __zhistarchive_have_datetime=0
typeset -gi __zhistarchive_have_jobparams=0
typeset -gi __zhistarchive_bg_enabled=0
typeset -gi __zhistarchive_chld_conflict=0
typeset -gi __zhistarchive_chained_user_chld=0
typeset -gi __zhistarchive_notify_was_on=0
typeset -gi __zhistarchive_fd=-1
typeset -gi __zhistarchive_seq=0
typeset -gi __zhistarchive_writing=0
(( ${+builtins} && ${+builtins[syswrite]} )) && __zhistarchive_have_syswrite=1
(( ${+epochtime} && ${+builtins} && ${+builtins[strftime]} )) && __zhistarchive_have_datetime=1
(( ${+jobstates} && ${+jobtexts} && ${+jobdirs} )) && __zhistarchive_have_jobparams=1
typeset -g __zhistarchive_session_id=""
typeset -g __zhistarchive_file=""
typeset -g __zhistarchive_dir=""
typeset -g __zhistarchive_cur_id=""
typeset -g __zhistarchive_host="${HOST:-${HOSTNAME:-unknown-host}}"
typeset -g __zhistarchive_user="${USER:-${USERNAME:-unknown-user}}"
typeset -g __zhistarchive_shell_pid="$$"
typeset -g __zhistarchive_now_s=0
typeset -g __zhistarchive_now_ns=0
typeset -g __zhistarchive_now_ts=""
typeset -ga __zhistarchive_queue=()
typeset -gA __zhistarchive_cmd_start_s=()
typeset -gA __zhistarchive_cmd_start_ns=()
typeset -gA __zhistarchive_job_cmd_id=()
typeset -gA __zhistarchive_job_start_s=()
typeset -gA __zhistarchive_job_start_ns=()
typeset -gA __zhistarchive_jobs_before_pids=()
if (( ${+sysparams} )) && [[ -n ${sysparams[pid]-} ]]; then
__zhistarchive_shell_pid="${sysparams[pid]}"
fi
__zhistarchive_now() {
emulate -L zsh
if (( ${+epochtime} )); then
local -a _t
_t=( "${epochtime[@]}" )
typeset -g __zhistarchive_now_s="${_t[1]}"
typeset -g __zhistarchive_now_ns="${_t[2]}"
else
typeset -g __zhistarchive_now_s=0
typeset -g __zhistarchive_now_ns=0
typeset -g __zhistarchive_now_ts=""
return 1
fi
if (( ${+builtins} && ${+builtins[strftime]} )); then
strftime -s __zhistarchive_now_ts '%Y-%m-%dT%H:%M:%S%z' "$__zhistarchive_now_s" "$__zhistarchive_now_ns" 2>/dev/null || \
typeset -g __zhistarchive_now_ts="$__zhistarchive_now_s"
else
typeset -g __zhistarchive_now_ts="$__zhistarchive_now_s"
fi
}
__zhistarchive_json_escape() {
emulate -L zsh
local s="$1"
s="${s//\\/\\\\}"
s="${s//\"/\\\"}"
s="${s//$'\001'/\\u0001}"
s="${s//$'\002'/\\u0002}"
s="${s//$'\003'/\\u0003}"
s="${s//$'\004'/\\u0004}"
s="${s//$'\005'/\\u0005}"
s="${s//$'\006'/\\u0006}"
s="${s//$'\007'/\\u0007}"
s="${s//$'\010'/\\b}"
s="${s//$'\011'/\\t}"
s="${s//$'\012'/\\n}"
s="${s//$'\013'/\\u000b}"
s="${s//$'\014'/\\f}"
s="${s//$'\015'/\\r}"
s="${s//$'\016'/\\u000e}"
s="${s//$'\017'/\\u000f}"
s="${s//$'\020'/\\u0010}"
s="${s//$'\021'/\\u0011}"
s="${s//$'\022'/\\u0012}"
s="${s//$'\023'/\\u0013}"
s="${s//$'\024'/\\u0014}"
s="${s//$'\025'/\\u0015}"
s="${s//$'\026'/\\u0016}"
s="${s//$'\027'/\\u0017}"
s="${s//$'\030'/\\u0018}"
s="${s//$'\031'/\\u0019}"
s="${s//$'\032'/\\u001a}"
s="${s//$'\033'/\\u001b}"
s="${s//$'\034'/\\u001c}"
s="${s//$'\035'/\\u001d}"
s="${s//$'\036'/\\u001e}"
s="${s//$'\037'/\\u001f}"
REPLY="$s"
}
__zhistarchive_json_string() {
emulate -L zsh
__zhistarchive_json_escape "$1"
REPLY="\"$REPLY\""
}
__zhistarchive_raw_write() {
emulate -L zsh
local line="$1"
(( __zhistarchive_enabled && __zhistarchive_fd >= 0 )) || return 1
if (( __zhistarchive_have_syswrite )); then
syswrite -o "$__zhistarchive_fd" "${line}"$'\n' 2>/dev/null && return 0
fi
builtin print -r -u "$__zhistarchive_fd" -- "$line" 2>/dev/null && return 0
__zhistarchive_enabled=0
return 1
}
__zhistarchive_write() {
emulate -L zsh
(( __zhistarchive_enabled && ${ZSH_SUBSHELL:-0} == 0 )) || return 0
local line="$1"
if (( __zhistarchive_writing )); then
__zhistarchive_queue+=( "$line" )
return 0
fi
__zhistarchive_writing=1
__zhistarchive_raw_write "$line" || true
local -a q
local queued
while (( ${#__zhistarchive_queue[@]} )); do
q=( "${__zhistarchive_queue[@]}" )
__zhistarchive_queue=()
for queued in "${q[@]}"; do
__zhistarchive_raw_write "$queued" || true
done
done
__zhistarchive_writing=0
return 0
}
__zhistarchive_duration_json_fields() {
emulate -L zsh
local start_s="$1" start_ns="$2" end_s="$3" end_ns="$4"
local d_s d_ns
if [[ -z $start_s || -z $start_ns || -z $end_s || -z $end_ns ]]; then
REPLY='"duration_s":null,"duration_ns":null'
return 0
fi
(( d_s = end_s - start_s ))
(( d_ns = end_ns - start_ns ))
if (( d_ns < 0 )); then
(( d_s-- ))
(( d_ns += 1000000000 ))
fi
if (( d_s < 0 )); then
REPLY='"duration_s":null,"duration_ns":null'
else
REPLY="\"duration_s\":$d_s,\"duration_ns\":$d_ns"
fi
}
__zhistarchive_status_from_proc_state() {
emulate -L zsh
local state="$1"
local raw
REPLY=null
REPLY2=unknown
if [[ $state == done ]]; then
REPLY=0
REPLY2=exit
elif [[ $state == exit\ * ]]; then
raw="${state#exit }"
case "$raw" in
(*[!0-9]*|'')
REPLY=null
REPLY2=unknown
;;
(*)
# zsh 5.10 source's jobstates formatter prints the raw wait status
# for nonzero exits. Other versions may print the real exit status.
# Treat 256,512,...,65280 as raw wait statuses; otherwise keep as-is.
if (( raw > 255 && raw % 256 == 0 )); then
REPLY=$(( raw / 256 ))
else
REPLY=$raw
fi
REPLY2=exit
;;
esac
fi
}
__zhistarchive_job_pids_csv() {
emulate -L zsh
local js="$1"
local -a parts pids
local i seg pid
parts=( "${(@s.:.)js}" )
pids=()
for (( i = 3; i <= ${#parts[@]}; i++ )); do
seg="${parts[$i]}"
pid="${seg%%=*}"
[[ -n $pid ]] && pids+=( "$pid" )
done
REPLY="${(j:,:)pids}"
}
__zhistarchive_processes_json() {
emulate -L zsh
local js="$1"
local -a parts items
local i seg pid state status_json status_kind qstate
parts=( "${(@s.:.)js}" )
items=()
for (( i = 3; i <= ${#parts[@]}; i++ )); do
seg="${parts[$i]}"
pid="${seg%%=*}"
state="${seg#*=}"
__zhistarchive_status_from_proc_state "$state"
status_json="$REPLY"
status_kind="$REPLY2"
__zhistarchive_json_string "$state"
qstate="$REPLY"
case "$pid" in
(""|*[!0-9]*)
__zhistarchive_json_string "$pid"
items+=( "{\"pid\":$REPLY,\"state\":$qstate,\"status\":$status_json,\"status_kind\":\"$status_kind\"}" )
;;
(*)
items+=( "{\"pid\":$pid,\"state\":$qstate,\"status\":$status_json,\"status_kind\":\"$status_kind\"}" )
;;
esac
done
REPLY="[${(j:,:)items}]"
}
__zhistarchive_job_summary_fields() {
emulate -L zsh
local job="$1" js="$2"
local -a parts
local job_state mark text dir pids qjob_state qmark qtext qdir processes_json
parts=( "${(@s.:.)js}" )
job_state="${parts[1]-}"
mark="${parts[2]-}"
text="${jobtexts[$job]-}"
dir="${jobdirs[$job]-}"
__zhistarchive_job_pids_csv "$js"; pids="$REPLY"
__zhistarchive_processes_json "$js"; processes_json="$REPLY"
__zhistarchive_json_string "$job_state"; qjob_state="$REPLY"
__zhistarchive_json_string "$mark"; qmark="$REPLY"
__zhistarchive_json_string "$text"; qtext="$REPLY"
__zhistarchive_json_string "$dir"; qdir="$REPLY"
REPLY="\"job\":$job,\"job_state\":$qjob_state,\"job_mark\":$qmark,\"job_pids\":\"$pids\",\"job_text\":$qtext,\"job_dir\":$qdir,\"processes\":$processes_json"
}
__zhistarchive_log_session_start() {
emulate -L zsh
__zhistarchive_now
local qsession qts qhost quser qfile qzver
__zhistarchive_json_string "$__zhistarchive_session_id"; qsession="$REPLY"
__zhistarchive_json_string "$__zhistarchive_now_ts"; qts="$REPLY"
__zhistarchive_json_string "$__zhistarchive_host"; qhost="$REPLY"
__zhistarchive_json_string "$__zhistarchive_user"; quser="$REPLY"
__zhistarchive_json_string "$__zhistarchive_file"; qfile="$REPLY"
__zhistarchive_json_string "${ZSH_VERSION:-}"; qzver="$REPLY"
local bg_json notify_json
(( __zhistarchive_bg_enabled )) && bg_json=true || bg_json=false
(( __zhistarchive_notify_was_on )) && notify_json=true || notify_json=false
__zhistarchive_write "{\"type\":\"session_start\",\"schema\":1,\"session_id\":$qsession,\"ts\":$qts,\"epoch_s\":$__zhistarchive_now_s,\"epoch_ns\":$__zhistarchive_now_ns,\"host\":$qhost,\"user\":$quser,\"shell_pid\":$__zhistarchive_shell_pid,\"zsh_version\":$qzver,\"file\":$qfile,\"background_tracking\":$bg_json,\"notify_was_on\":$notify_json}"
}
__zhistarchive_log_command_start() {
emulate -L zsh
local id="$1" typed="$2" expanded="$3" short="$4"
local qid qsession qts qhost quser qcwd qtty qtyped qexpanded qshort
__zhistarchive_json_string "$id"; qid="$REPLY"
__zhistarchive_json_string "$__zhistarchive_session_id"; qsession="$REPLY"
__zhistarchive_json_string "$__zhistarchive_now_ts"; qts="$REPLY"
__zhistarchive_json_string "$__zhistarchive_host"; qhost="$REPLY"
__zhistarchive_json_string "$__zhistarchive_user"; quser="$REPLY"
__zhistarchive_json_string "$PWD"; qcwd="$REPLY"
__zhistarchive_json_string "${TTY:-}"; qtty="$REPLY"
__zhistarchive_json_string "$typed"; qtyped="$REPLY"
__zhistarchive_json_string "$expanded"; qexpanded="$REPLY"
__zhistarchive_json_string "$short"; qshort="$REPLY"
__zhistarchive_write "{\"type\":\"command_start\",\"schema\":1,\"id\":$qid,\"session_id\":$qsession,\"seq\":$__zhistarchive_seq,\"ts\":$qts,\"epoch_s\":$__zhistarchive_now_s,\"epoch_ns\":$__zhistarchive_now_ns,\"host\":$qhost,\"user\":$quser,\"cwd\":$qcwd,\"tty\":$qtty,\"shell_pid\":$__zhistarchive_shell_pid,\"histno\":${HISTNO:-0},\"command\":$qtyped,\"command_expanded\":$qexpanded,\"command_short\":$qshort}"
}
__zhistarchive_log_command_end() {
emulate -L zsh
local id="$1" _status="$2" async_jobs_json="$3" reason="${4:-precmd}"
__zhistarchive_now
local qid qsession qts qcwd qreason duration_fields
__zhistarchive_json_string "$id"; qid="$REPLY"
__zhistarchive_json_string "$__zhistarchive_session_id"; qsession="$REPLY"
__zhistarchive_json_string "$__zhistarchive_now_ts"; qts="$REPLY"
__zhistarchive_json_string "$PWD"; qcwd="$REPLY"
__zhistarchive_json_string "$reason"; qreason="$REPLY"
__zhistarchive_duration_json_fields "${__zhistarchive_cmd_start_s[$id]-}" "${__zhistarchive_cmd_start_ns[$id]-}" "$__zhistarchive_now_s" "$__zhistarchive_now_ns"
duration_fields="$REPLY"
[[ -z $async_jobs_json ]] && async_jobs_json="[]"
__zhistarchive_write "{\"type\":\"command_end\",\"schema\":1,\"id\":$qid,\"session_id\":$qsession,\"ts\":$qts,\"epoch_s\":$__zhistarchive_now_s,\"epoch_ns\":$__zhistarchive_now_ns,\"status\":$_status,\"cwd\":$qcwd,\"async_jobs\":$async_jobs_json,\"reason\":$qreason,$duration_fields}"
unset "__zhistarchive_cmd_start_s[$id]" "__zhistarchive_cmd_start_ns[$id]"
}
__zhistarchive_log_async_start() {
emulate -L zsh
local id="$1" job="$2" js="$3"
__zhistarchive_now
local qid qsession qts job_fields
__zhistarchive_json_string "$id"; qid="$REPLY"
__zhistarchive_json_string "$__zhistarchive_session_id"; qsession="$REPLY"
__zhistarchive_json_string "$__zhistarchive_now_ts"; qts="$REPLY"
__zhistarchive_job_summary_fields "$job" "$js"; job_fields="$REPLY"
__zhistarchive_write "{\"type\":\"async_start\",\"schema\":1,\"id\":$qid,\"session_id\":$qsession,\"ts\":$qts,\"epoch_s\":$__zhistarchive_now_s,\"epoch_ns\":$__zhistarchive_now_ns,$job_fields}"
}
__zhistarchive_log_async_end() {
emulate -L zsh
local id="$1" job="$2" js="$3"
__zhistarchive_now
local qid qsession qts job_fields duration_fields last_state status_json status_kind
local -a parts
parts=( "${(@s.:.)js}" )
if (( ${#parts[@]} >= 3 )); then
last_state="${parts[-1]#*=}"
else
last_state=""
fi
__zhistarchive_status_from_proc_state "$last_state"
status_json="$REPLY"
status_kind="$REPLY2"
__zhistarchive_json_string "$id"; qid="$REPLY"
__zhistarchive_json_string "$__zhistarchive_session_id"; qsession="$REPLY"
__zhistarchive_json_string "$__zhistarchive_now_ts"; qts="$REPLY"
__zhistarchive_job_summary_fields "$job" "$js"; job_fields="$REPLY"
__zhistarchive_duration_json_fields "${__zhistarchive_job_start_s[$job]-}" "${__zhistarchive_job_start_ns[$job]-}" "$__zhistarchive_now_s" "$__zhistarchive_now_ns"
duration_fields="$REPLY"
__zhistarchive_write "{\"type\":\"async_end\",\"schema\":1,\"id\":$qid,\"session_id\":$qsession,\"ts\":$qts,\"epoch_s\":$__zhistarchive_now_s,\"epoch_ns\":$__zhistarchive_now_ns,\"status\":$status_json,\"status_kind\":\"$status_kind\",$job_fields,$duration_fields}"
}
__zhistarchive_log_async_lost() {
emulate -L zsh
local id="$1" job="$2" reason="$3"
__zhistarchive_now
local qid qsession qts qreason duration_fields
__zhistarchive_json_string "$id"; qid="$REPLY"
__zhistarchive_json_string "$__zhistarchive_session_id"; qsession="$REPLY"
__zhistarchive_json_string "$__zhistarchive_now_ts"; qts="$REPLY"
__zhistarchive_json_string "$reason"; qreason="$REPLY"
__zhistarchive_duration_json_fields "${__zhistarchive_job_start_s[$job]-}" "${__zhistarchive_job_start_ns[$job]-}" "$__zhistarchive_now_s" "$__zhistarchive_now_ns"
duration_fields="$REPLY"
__zhistarchive_write "{\"type\":\"async_lost\",\"schema\":1,\"id\":$qid,\"session_id\":$qsession,\"ts\":$qts,\"epoch_s\":$__zhistarchive_now_s,\"epoch_ns\":$__zhistarchive_now_ns,\"job\":$job,\"reason\":$qreason,$duration_fields}"
}
__zhistarchive_process_done_jobs() {
local _save_status=$?
emulate -L zsh
(( __zhistarchive_enabled && __zhistarchive_bg_enabled && ${ZSH_SUBSHELL:-0} == 0 )) || return $_save_status
local job id js job_state
for job in "${(@k)__zhistarchive_job_cmd_id}"; do
id="${__zhistarchive_job_cmd_id[$job]}"
if (( ! ${+jobstates[$job]} )); then
__zhistarchive_log_async_lost "$id" "$job" "missing_from_job_table"
unset "__zhistarchive_job_cmd_id[$job]" "__zhistarchive_job_start_s[$job]" "__zhistarchive_job_start_ns[$job]"
continue
fi
js="${jobstates[$job]}"
job_state="${js%%:*}"
if [[ $job_state == done ]]; then
__zhistarchive_log_async_end "$id" "$job" "$js"
unset "__zhistarchive_job_cmd_id[$job]" "__zhistarchive_job_start_s[$job]" "__zhistarchive_job_start_ns[$job]"
fi
done
return $_save_status
}
__zhistarchive_preexec() {
local _save_status=$?
emulate -L zsh
(( __zhistarchive_enabled && ${ZSH_SUBSHELL:-0} == 0 )) || return $_save_status
__zhistarchive_process_done_jobs
__zhistarchive_now
(( __zhistarchive_seq++ ))
local id="${__zhistarchive_session_id}.${__zhistarchive_seq}"
local typed="$1" short="$2" expanded="$3"
[[ -z $typed ]] && typed="$expanded"
__zhistarchive_cur_id="$id"
__zhistarchive_cmd_start_s[$id]="$__zhistarchive_now_s"
__zhistarchive_cmd_start_ns[$id]="$__zhistarchive_now_ns"
__zhistarchive_jobs_before_pids=()
if (( __zhistarchive_bg_enabled )); then
local j
for j in "${(@k)jobstates}"; do
__zhistarchive_job_pids_csv "${jobstates[$j]}"
__zhistarchive_jobs_before_pids[$j]="$REPLY"
done
fi
__zhistarchive_log_command_start "$id" "$typed" "$expanded" "$short"
return $_save_status
}
__zhistarchive_precmd() {
local last_status=$?
emulate -L zsh
(( __zhistarchive_enabled && ${ZSH_SUBSHELL:-0} == 0 )) || return $last_status
local id="$__zhistarchive_cur_id"
local -a new_jobs async_jobs
local j current_pids before_pids js async_jobs_json
new_jobs=()
async_jobs=()
if [[ -n $id ]]; then
if (( __zhistarchive_bg_enabled )); then
for j in "${(@k)jobstates}"; do
__zhistarchive_job_pids_csv "${jobstates[$j]}"
current_pids="$REPLY"
before_pids="${__zhistarchive_jobs_before_pids[$j]-}"
if [[ -z ${__zhistarchive_jobs_before_pids[$j]+x} || $current_pids != $before_pids ]]; then
new_jobs+=( "$j" )
fi
done
fi
if (( ${#new_jobs[@]} )); then
local -a items
items=()
for j in "${new_jobs[@]}"; do
js="${jobstates[$j]}"
__zhistarchive_job_cmd_id[$j]="$id"
__zhistarchive_job_start_s[$j]="${__zhistarchive_cmd_start_s[$id]-}"
__zhistarchive_job_start_ns[$j]="${__zhistarchive_cmd_start_ns[$id]-}"
__zhistarchive_log_async_start "$id" "$j" "$js"
case "$j" in
(""|*[!0-9]*)
__zhistarchive_json_string "$j"
items+=( "$REPLY" )
;;
(*)
items+=( "$j" )
;;
esac
done
async_jobs_json="[${(j:,:)items}]"
else
async_jobs_json="[]"
fi
__zhistarchive_cur_id=""
__zhistarchive_log_command_end "$id" "$last_status" "$async_jobs_json" "precmd"
fi
__zhistarchive_process_done_jobs
return $last_status
}
__zhistarchive_zshexit() {
local exit_status=$?
emulate -L zsh
(( __zhistarchive_enabled && ${ZSH_SUBSHELL:-0} == 0 )) || return $exit_status
if [[ -n $__zhistarchive_cur_id ]]; then
__zhistarchive_log_command_end "$__zhistarchive_cur_id" "$exit_status" "[]" "zshexit"
__zhistarchive_cur_id=""
fi
__zhistarchive_process_done_jobs
__zhistarchive_now
local qsession qts
__zhistarchive_json_string "$__zhistarchive_session_id"; qsession="$REPLY"
__zhistarchive_json_string "$__zhistarchive_now_ts"; qts="$REPLY"
__zhistarchive_write "{\"type\":\"session_end\",\"schema\":1,\"session_id\":$qsession,\"ts\":$qts,\"epoch_s\":$__zhistarchive_now_s,\"epoch_ns\":$__zhistarchive_now_ns,\"status\":$exit_status}"
if (( __zhistarchive_fd >= 0 )); then
exec {__zhistarchive_fd}>&- 2>/dev/null || true
__zhistarchive_fd=-1
fi
return $exit_status
}
__zhistarchive_trap_chld() {
local _save_status=$?
emulate -L zsh
(( ${ZSH_SUBSHELL:-0} == 0 )) && __zhistarchive_process_done_jobs
if (( ${+functions[__zhistarchive_user_TRAPCHLD]} )); then
__zhistarchive_user_TRAPCHLD "$@" || true
fi
return $_save_status
}
# Compute session id and open the per-session file. The archive requires
# zsh/datetime; normal zsh history timestamping above still works without it.
if (( __zhistarchive_have_datetime )); then
__zhistarchive_now
typeset __zhistarchive_safe_host="${__zhistarchive_host//[^A-Za-z0-9_.-]/_}"
typeset __zhistarchive_safe_user="${__zhistarchive_user//[^A-Za-z0-9_.-]/_}"
__zhistarchive_session_id="${__zhistarchive_safe_host}.${__zhistarchive_safe_user}.${__zhistarchive_now_s}.${__zhistarchive_now_ns}.${__zhistarchive_shell_pid}.${RANDOM}${RANDOM}"
__zhistarchive_dir="${ZSH_HISTORY_ARCHIVE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/zsh-history-archive}"
__zhistarchive_file="${__zhistarchive_dir}/hist.${__zhistarchive_session_id}.jsonl"
typeset __zhistarchive_old_umask
__zhistarchive_old_umask="$(umask)"
umask 077
[[ -d $__zhistarchive_dir ]] || command mkdir -p -- "$__zhistarchive_dir" 2>/dev/null || true
if [[ -d $__zhistarchive_dir ]]; then
if (( ${+builtins} && ${+builtins[sysopen]} )); then
sysopen -w -a -m 0600 -o create,cloexec -u __zhistarchive_fd "$__zhistarchive_file" 2>/dev/null || __zhistarchive_fd=-1
else
exec {__zhistarchive_fd}>>"$__zhistarchive_file" 2>/dev/null || __zhistarchive_fd=-1
fi
fi
umask "$__zhistarchive_old_umask"
if (( __zhistarchive_fd >= 0 )); then
__zhistarchive_enabled=1
fi
fi
# Enable background tracking only if we have zsh job parameters and no
# unchainable pre-existing list-form CHLD trap.
if (( __zhistarchive_enabled && __zhistarchive_have_jobparams )) && [[ -o monitor ]]; then
if (( ${+functions[TRAPCHLD]} )) && [[ ${functions[TRAPCHLD]} != *__zhistarchive_trap_chld* ]]; then
functions -c TRAPCHLD __zhistarchive_user_TRAPCHLD 2>/dev/null && __zhistarchive_chained_user_chld=1
elif [[ -n $(trap -p CHLD 2>/dev/null) ]] && (( ! ${+functions[TRAPCHLD]} )); then
__zhistarchive_chld_conflict=1
fi
if (( ! __zhistarchive_chld_conflict )); then
__zhistarchive_bg_enabled=1
[[ -o notify ]] && __zhistarchive_notify_was_on=1
unsetopt NOTIFY
TRAPCHLD() { __zhistarchive_trap_chld "$@"; }
fi
fi
if (( __zhistarchive_enabled )); then
add-zsh-hook preexec __zhistarchive_preexec
add-zsh-hook precmd __zhistarchive_precmd
add-zsh-hook zshexit __zhistarchive_zshexit
__zhistarchive_log_session_start
fi
fi
@ivan
Copy link
Copy Markdown
Author

ivan commented May 3, 2026

Please correctly implement the thing below, for installation to zshrc. Ignore the PostgreSQL stuff. Then review your implementation.

--

I remember now that the main problem with my zsh setup is that

  1. it doesn't log commands with a timestamp to the history, so I don't know when I ran a command
  2. there's no process for importing such timestamped logs from every machine to one PostgreSQL database, to retain command history forever

therefore in addition to the standard zsh history it should probably log
(unique id, timestamp, hostname, username, cwd, command) before running a command and
(unique id, timestamp, exit status) after the command ends

--
https://github.com/larkery/zsh-histdb/blob/master/sqlite-history.zsh
https://news.ycombinator.com/item?id=23470506
probably has performance issues compared to appending a new line to a file, so let's just append lines to a 'history archive' file.

--

There's a JSON implementation in your thinking trace, and indeed I think it should use JSON because we have heterogeneous record types, and because we might want additional fields in the future. Note that when appending 4096 bytes or less to a file on Linux, we don't need to lock the file to avoid corruption. But some of our commands and therefore records are longer than 4096 bytes. We don't want to lock the archive history file because that might stall us entirely if another shell has hung. So maybe the implementation should write to a unique file, one for each zsh? Please implement that, and also make sure all of this works correctly with backgrounded i.e. &'ed processes that finish in a different order than in which we started them.

--

search Google and GitHub and sourcegraph and see if there's any existing project that does this about as well as the implementation above?

--

yeah, do lots of code searches on the things you implemented, maybe you can find your own training data :)

--

Now that you're 5.5 Pro instead of 5.2, please review all those ideas and implement a bug-free thing for zsh that solves the history logging problem as per my first two messages. If you're not sure about something, either mark it XXX or don't implement it. I've attaching the zsh source code for your reference.

(zsh-master.zip)

--

# source zsh-history-archive.zsh
__zhistarchive_write:2: bad math expression: operator expected at 0 '
__zhistarchive_precmd:3: bad math expression: operator expected at 0 '

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment