cd /news/ai-tools/caffeinate-ai-keep-macos-awake-while… · home › topics › ai-tools › article
[ARTICLE · art-143582] src=gist.github.com ↗ pub= topic=ai-tools verified=true sentiment=↑ positive

Caffeinate AI: Keep macOS awake while one or more monitored apps are running (for long agentic sessions with ChatGPT/Codex or Claude)

A developer released caffeinate-ai, a zsh script that keeps macOS from idle-sleeping while monitored applications such as ChatGPT and Codex are running, targeting long agentic coding sessions. The tool polls running apps every five seconds and wraps macOS's built-in caffeinate with -i so the system stays awake while the display can still sleep, and it ships with start, stop, restart, toggle, status, doctor, and run subcommands plus environment-variable configuration for interval, match mode, and target app list.

by read35 min views1 publishedOct 2, 2026

| | #!/bin/zsh | | | # | | | # caffeinate-ai | | | # | | | # Keep macOS awake while one or more monitored applications are running. | | | # | | | # Setup: | | | # 1. Add to local bin dir: ~/.local/bin/caffeinate-ai | | | # 2. Make it executable: chmod +x ~/.local/bin/caffeinate-ai | | | # 3. View help and usage: caffeinate-ai --help | | | # | | | # Defaults: | | | # - Monitor ChatGPT/Codex #TODO: add Claude | | | # - Poll every 5 seconds | | | # - Prevent idle system sleep with caffeinate -i | | | # - Allow the display to sleep normally | | | # - Produce no background logs unless --verbose is supplied | | | # | | | # Commands: | | | # caffeinate-ai --help | | | # caffeinate-ai start | | | # caffeinate-ai stop | | | # caffeinate-ai restart | | | # caffeinate-ai toggle | | | # caffeinate-ai status | | | # caffeinate-ai doctor | | | # caffeinate-ai run | | | # caffeinate-ai | | | # When called without a command, defaults to caffeinate-ai run, | | | # which runs caffeinate-ai in the foreground of the current shell. | | | set -u | | | set -o pipefail | | | # ----------------------------------------------------------------------------- | | | # Constants |

|  | # ----------------------------------------------------------------------------- | 
|  | readonly SCRIPT_PATH="${0:A}" | 
|  | readonly SCRIPT_NAME="${0:t}" | 
|  | readonly STATE_DIR="${HOME}/Library/Caches/caffeinate-ai" | 
|  | readonly PID_FILE="${STATE_DIR}/caffeinate-ai.pid" | 
|  | readonly LOCK_DIR="${STATE_DIR}/operation.lock" | 
|  | readonly LOCK_PID_FILE="${LOCK_DIR}/pid" | 
|  | readonly LOG_FILE="${STATE_DIR}/caffeinate-ai.log" | 
|  | # ----------------------------------------------------------------------------- | 

| | # Defaults / environment configuration | | | # ----------------------------------------------------------------------------- | | | # | | | # These defaults can be overridden by environment variables: | | | # | | | # CAFFEINATE_AI_INTERVAL=10 | | | # CAFFEINATE_AI_MATCH_MODE=exact | | | # CAFFEINATE_AI_APPS='ChatGPT:Codex:Visual Studio Code' | | | # CAFFEINATE_AI_CAFFEINATE_ARGS='-i' | | | # | | | # CLI arguments take precedence over environment configuration. | | | # |

|  | POLL_INTERVAL="${CAFFEINATE_AI_INTERVAL:-5}" | 
|  | MATCH_MODE="${CAFFEINATE_AI_MATCH_MODE:-exact}" | 

| | VERBOSE=false | | | DRY_RUN=false | | | # | | | # Add other applications to monitor here | | | # | | | TARGET_APPS=( | | | "ChatGPT" | | | "Codex" | | | ) | | | CAFFEINATE_ARGS=( | | | "-i" | | | ) |

|  | if [[ -n "${CAFFEINATE_AI_APPS:-}" ]]; then | 
|  | TARGET_APPS=( | 
|  | "${(@s/:/)CAFFEINATE_AI_APPS}" | 

| | ) | | | fi |

|  | if [[ -n "${CAFFEINATE_AI_CAFFEINATE_ARGS:-}" ]]; then | 
|  | CAFFEINATE_ARGS=( | 
|  | "${(@s/:/)CAFFEINATE_AI_CAFFEINATE_ARGS}" | 

| | ) | | | fi | | | # | | | # PID of the caffeinate process owned by this watcher. | | | # | | | CAFFEINATE_PID="" | | | # | | | # Set to 1 only for a detached watcher launched by start. | | | # |

|  | IS_DAEMON="${CAFFEINATE_AI_DAEMON:-0}" | 
|  | # ----------------------------------------------------------------------------- | 

| | # Help |

|  | # ----------------------------------------------------------------------------- | 
|  | usage() { | 

| | cat <<EOF | | | Usage: | | | ${SCRIPT_NAME} [command] [options] | | | Commands: | | | start [options] | | | Start caffeinate-ai as a detached background watcher. | | | stop | | | Stop the background watcher and its caffeinate child. | | | restart [options] | | | Stop the current watcher and start a new one. | | | toggle [options] | | | Stop the watcher if running; otherwise start it. | | | Options are used only when starting. | | | status | | | Show the state of the background watcher and caffeinate child. | | | doctor [options] | | | Diagnose process detection, watcher state, and the macOS power | | | assertion. | | | run [options] | | | Run the watcher in the foreground. | | | This is the default when options are supplied without a command. | | | help | | | Show this help message. | | | Options for start, restart, toggle, run, and doctor: | | | -a, --app NAME | | | Add an application/process name to monitor. | | | Supplying --app replaces the default ChatGPT/Codex list. | | | May be specified multiple times: |

|  | ${SCRIPT_NAME} start \\ | 
|  | --app ChatGPT \\ | 
|  | --app Codex | 
|  | -i, --interval SECONDS | 

| | Poll interval. | | | Default: |

|  | ${POLL_INTERVAL} | 
|  | -m, --match MODE | 

| | Process matching mode. | | | Supported: | | | exact | | | Exact process-name matching using pgrep -x. | | | contains | | | Match against the full process command using pgrep -f. | | | Default: |

|  | ${MATCH_MODE} | 
|  | -c, --caffeinate ARG | 

| | Add an argument passed to caffeinate. | | | Supplying this option replaces the default "-i". | | | May be specified multiple times: |

|  | ${SCRIPT_NAME} start \\ | 
|  | --caffeinate=-d \\ | 
|  | --caffeinate=-i | 
|  | -v, --verbose | 

| | Enable diagnostic output. | | | In background mode, verbose output is written to: | | | ${LOG_FILE} | | | The log is truncated each time a new verbose daemon starts. | | | --dry-run | | | Foreground-only mode. | | | Detect applications and report when caffeinate would start or stop, | | | without creating a power assertion. | | | -h, --help | | | Show this help message. | | | Environment variables: | | | CAFFEINATE_AI_INTERVAL | | | Default polling interval. | | | CAFFEINATE_AI_MATCH_MODE | | | Default matching mode: exact or contains. | | | CAFFEINATE_AI_APPS | | | Colon-separated default application names. | | | Example: | | | export CAFFEINATE_AI_APPS='ChatGPT:Codex:Visual Studio Code' | | | CAFFEINATE_AI_CAFFEINATE_ARGS | | | Colon-separated caffeinate arguments. | | | Example: | | | export CAFFEINATE_AI_CAFFEINATE_ARGS='-i' | | | Examples: | | | Start: | | | ${SCRIPT_NAME} start | | | Status: | | | ${SCRIPT_NAME} status | | | Stop: | | | ${SCRIPT_NAME} stop | | | Restart: | | | ${SCRIPT_NAME} restart | | | Toggle: | | | ${SCRIPT_NAME} toggle | | | Diagnose: | | | ${SCRIPT_NAME} doctor | | | Run in the foreground: |

|  | ${SCRIPT_NAME} run | 
|  | Dry-run process detection: | 
|  | ${SCRIPT_NAME} run --dry-run | 

| | Run verbosely: | | | ${SCRIPT_NAME} run --verbose | | | Start with verbose background logging: | | | ${SCRIPT_NAME} start --verbose | | | Poll every 10 seconds: | | | ${SCRIPT_NAME} start --interval 10 | | | Watch only Codex: | | | ${SCRIPT_NAME} start --app Codex | | | Watch several applications: |

|  | ${SCRIPT_NAME} start \\ | 
|  | --app ChatGPT \\ | 
|  | --app Codex \\ | 
|  | --app "Visual Studio Code" | 

| | Prevent both idle system sleep and display sleep: |

|  | ${SCRIPT_NAME} start \\ | 
|  | --caffeinate=-d \\ | 
|  | --caffeinate=-i | 

| | State: | | | PID: | | | ${PID_FILE} | | | Lock: | | | ${LOCK_DIR} | | | Verbose log: | | | ${LOG_FILE} | | | Exit codes: | | | 0 Success | | | 1 Runtime failure or inactive status | | | 2 Invalid arguments | | | 127 Required command not found | | | EOF | | | } | | | # ----------------------------------------------------------------------------- | | | # Output |

|  | # ----------------------------------------------------------------------------- | 
|  | print_status() { | 
|  | print -r -- "$*" | 

| | } |

|  | log() { | 
|  | if [[ "${VERBOSE}" != true ]]; then | 

| | return 0 | | | fi |

|  | printf '[%s] %s\n' \ | 
|  | "$(date '+%Y-%m-%d %H:%M:%S')" \ | 

| | "$*" | | | } |

|  | debug() { | 
|  | if [[ "${VERBOSE}" == true ]]; then | 

| | log "DEBUG: $*" | | | fi | | | } |

|  | error() { | 
|  | printf '%s: error: %s\n' "${SCRIPT_NAME}" "$*" >&2 | 

| | } | | | die() { | | | local message="$1" |

|  | local exit_code="${2:-1}" | 
|  | error "${message}" | 
|  | exit "${exit_code}" | 

| | } | | | # ----------------------------------------------------------------------------- | | | # Dependency / input validation |

|  | # ----------------------------------------------------------------------------- | 
|  | require_command() { | 

| | local command_name="$1" | | | if ! command -v "${command_name}" >/dev/null 2>&1; then | | | die "required command not found: ${command_name}" 127 | | | fi | | | } | | | validate_positive_integer() { | | | local value="$1" | | | local name="$2" |

|  | if [[ ! "${value}" =~ '^[0-9]+$' ]]; then | 
|  | die "${name} must be a positive integer; received: ${value}" 2 | 

| | fi |

|  | if (( value < 1 )); then | 
|  | die "${name} must be greater than zero; received: ${value}" 2 | 

| | fi | | | } | | | validate_runtime_dependencies() { | | | require_command caffeinate | | | require_command pgrep | | | require_command ps | | | require_command date | | | require_command kill | | | require_command sleep | | | require_command mkdir | | | require_command mv | | | require_command rm | | | require_command rmdir | | | } | | | validate_lifecycle_dependencies() { | | | require_command pgrep | | | require_command ps | | | require_command kill | | | require_command sleep | | | require_command mkdir | | | require_command rm | | | require_command rmdir | | | } | | | validate_doctor_dependencies() { | | | validate_lifecycle_dependencies | | | require_command pmset | | | require_command grep | | | } | | | # ----------------------------------------------------------------------------- | | | # State directory |

|  | # ----------------------------------------------------------------------------- | 
|  | ensure_state_dir() { | 
|  | if ! mkdir -p "${STATE_DIR}"; then | 

| | die "could not create state directory: ${STATE_DIR}" | | | fi | | | } | | | # ----------------------------------------------------------------------------- | | | # Generic process inspection |

|  | # ----------------------------------------------------------------------------- | 
|  | process_is_alive() { | 

| | local pid="$1" | | | kill -0 "${pid}" 2>/dev/null | | | } | | | process_command() { | | | local pid="$1" | | | ps -p "${pid}" -o command= 2>/dev/null | | | } | | | process_matches_script_instance() { | | | local pid="$1" | | | if ! process_is_alive "${pid}"; then | | | return 1 | | | fi | | | local command_line | | | if ! command_line="$(process_command "${pid}")"; then | | | return 1 | | | fi |

|  | if [[ "${command_line}" == *"${SCRIPT_PATH}"* \|\| | 
|  | "${command_line}" == *"${SCRIPT_NAME}"* ]]; then | 

| | return 0 | | | fi | | | return 1 | | | } | | | # ----------------------------------------------------------------------------- | | | # Atomic operation lock |

|  | # ----------------------------------------------------------------------------- | 
|  | acquire_operation_lock() { | 

| | validate_lifecycle_dependencies | | | ensure_state_dir | | | local attempts=2 |

|  | while (( attempts > 0 )); do | 
|  | if mkdir "${LOCK_DIR}" 2>/dev/null; then | 
|  | if ! print -r -- "$$" >"${LOCK_PID_FILE}"; then | 
|  | rmdir "${LOCK_DIR}" 2>/dev/null \|\| true | 

| | die "could not write operation lock PID" | | | fi | | | return 0 | | | fi | | | # | | | # Another process may have created the directory but not yet written the | | | # PID file. Give it a brief opportunity to finish. | | | # | | | if [[ ! -f "${LOCK_PID_FILE}" ]]; then | | | sleep 0.1 | | | fi | | | local owner_pid="" |

|  | if [[ -f "${LOCK_PID_FILE}" ]]; then | 
|  | owner_pid="$(<"${LOCK_PID_FILE}")" | 

| | fi | | | if [[ "${owner_pid}" =~ '[1]+$' ]] && | | | process_matches_script_instance "${owner_pid}"; then | | | die \ | | | "another caffeinate-ai operation is already in progress (PID ${owner_pid})" | | | fi | | | # | | | # The lock owner no longer exists or the lock metadata is invalid. | | | # |

|  | rm -f "${LOCK_PID_FILE}" 2>/dev/null \|\| true | 
|  | if ! rmdir "${LOCK_DIR}" 2>/dev/null; then | 

| | die "could not recover stale operation lock: ${LOCK_DIR}" | | | fi | | | (( attempts-- )) | | | done | | | die "could not acquire operation lock" | | | } |

|  | release_operation_lock() { | 
|  | if [[ ! -d "${LOCK_DIR}" ]]; then | 

| | return 0 | | | fi | | | local owner_pid="" |

|  | if [[ -f "${LOCK_PID_FILE}" ]]; then | 
|  | owner_pid="$(<"${LOCK_PID_FILE}")" | 

| | fi | | | # | | | # Only remove a valid lock owned by this process. | | | # | | | if [[ "${owner_pid}" != "$$" ]]; then | | | return 0 | | | fi |

|  | rm -f "${LOCK_PID_FILE}" 2>/dev/null \|\| true | 
|  | rmdir "${LOCK_DIR}" 2>/dev/null \|\| true | 

| | } | | | with_operation_lock() { | | | acquire_operation_lock | | | # | | | # Ensure an unexpected exit still releases our lock. | | | # | | | trap 'release_operation_lock' EXIT | | | trap 'release_operation_lock; exit 130' INT | | | trap 'release_operation_lock; exit 143' TERM | | | trap 'release_operation_lock; exit 129' HUP | | | "$@" | | | local exit_status=$? | | | release_operation_lock | | | trap - EXIT | | | trap - INT | | | trap - TERM | | | trap - HUP | | | return "${exit_status}" | | | } | | | # ----------------------------------------------------------------------------- | | | # PID file |

|  | # ----------------------------------------------------------------------------- | 
|  | read_daemon_pid() { | 
|  | if [[ ! -f "${PID_FILE}" ]]; then | 

| | return 1 | | | fi | | | local pid |

|  | pid="$(<"${PID_FILE}")" | 
|  | if [[ ! "${pid}" =~ '^[0-9]+$' ]]; then | 
|  | debug "invalid PID file; removing ${PID_FILE}" | 
|  | rm -f "${PID_FILE}" | 

| | return 1 | | | fi | | | print -r -- "${pid}" | | | } | | | write_daemon_pid() { | | | local pid="$1" | | | local temp_file="${PID_FILE}.${pid}.tmp" | | | ensure_state_dir | | | if ! print -r -- "${pid}" >"${temp_file}"; then | | | die "could not write temporary PID file: ${temp_file}" | | | fi |

|  | if ! mv -f "${temp_file}" "${PID_FILE}"; then | 
|  | rm -f "${temp_file}" | 
|  | die "could not write PID file: ${PID_FILE}" | 

| | fi | | | } | | | remove_pid_file_if_owned() { | | | local expected_pid="$1" | | | if [[ ! -f "${PID_FILE}" ]]; then | | | return 0 | | | fi | | | local pid |

|  | pid="$(<"${PID_FILE}")" | 
|  | if [[ "${pid}" == "${expected_pid}" ]]; then | 
|  | rm -f "${PID_FILE}" | 

| | fi | | | } | | | # ----------------------------------------------------------------------------- | | | # Daemon validation |

|  | # ----------------------------------------------------------------------------- | 
|  | process_matches_daemon() { | 

| | local pid="$1" | | | if ! process_matches_script_instance "${pid}"; then | | | return 1 | | | fi | | | local command_line | | | if ! command_line="$(process_command "${pid}")"; then | | | return 1 | | | fi | | | # | | | # A managed daemon is always re-executed using the explicit "run" | | | # subcommand. | | | # | | | if [[ "${command_line}" != " run" ]]; then | | | return 1 | | | fi | | | return 0 | | | } | | | daemon_is_running() { | | | local pid | | | if ! pid="$(read_daemon_pid)"; then | | | return 1 | | | fi | | | if process_matches_daemon "${pid}"; then | | | return 0 | | | fi | | | debug "removing stale PID file for PID ${pid}" | | | rm -f "${PID_FILE}" | | | return 1 | | | } | | | # ----------------------------------------------------------------------------- | | | # Target process detection |

|  | # ----------------------------------------------------------------------------- | 
|  | get_matching_pids() { | 

| | local process_name="$1" | | | local raw_pids="" | | | case "${MATCH_MODE}" in | | | exact) | | | raw_pids="$( | | | pgrep \ | | | -U "${UID}" \ | | | -x \ | | | -- "${process_name}" \ | | | 2>/dev/null || | | | true | | | )" | | | ;; | | | contains) | | | raw_pids="$( | | | pgrep \ | | | -U "${UID}" \ | | | -f \ | | | -- "${process_name}" \ | | | 2>/dev/null || | | | true | | | )" | | | ;; | | | *) | | | return 2 | | | ;; | | | esac | | | if [[ -z "${raw_pids}" ]]; then | | | return 1 | | | fi | | | local pid | | | for pid in ${(f)raw_pids}; do | | | # | | | # Do not allow the watcher or its invoking shell to satisfy its own | | | # contains-mode query. | | | # | | | if [[ "${pid}" == "$$" || "${pid}" == "${PPID}" ]]; then | | | continue | | | fi | | | print -r -- "${pid}" | | | done | | | } | | | process_is_running() { | | | local process_name="$1" | | | local matches |

|  | matches="$(get_matching_pids "${process_name}" 2>/dev/null \|\| true)" | 
|  | [[ -n "${matches}" ]] | 

| | } | | | find_running_target() { | | | local app |

|  | for app in "${TARGET_APPS[@]}"; do | 
|  | if process_is_running "${app}"; then | 
|  | print -r -- "${app}" | 

| | return 0 | | | fi | | | done | | | return 1 | | | } | | | # ----------------------------------------------------------------------------- | | | # caffeinate lifecycle |

|  | # ----------------------------------------------------------------------------- | 
|  | caffeinate_is_running() { | 
|  | if [[ -z "${CAFFEINATE_PID}" ]]; then | 

| | return 1 | | | fi | | | process_is_alive "${CAFFEINATE_PID}" | | | } | | | start_caffeinate() { | | | if caffeinate_is_running; then | | | debug "caffeinate already running as PID ${CAFFEINATE_PID}" | | | return 0 | | | fi | | | CAFFEINATE_PID="" |

|  | debug "starting: caffeinate ${(j: :)CAFFEINATE_ARGS}" | 
|  | caffeinate "${CAFFEINATE_ARGS[@]}" & | 

| | local pid=$! | | | # | | | # Give caffeinate a short opportunity to reject invalid arguments. | | | # | | | sleep 0.1 |

|  | if ! process_is_alive "${pid}"; then | 
|  | wait "${pid}" 2>/dev/null | 

| | local exit_status=$? | | | error "caffeinate failed to start (exit status ${exit_status})" | | | return 1 | | | fi |

|  | CAFFEINATE_PID="${pid}" | 
|  | log "Started caffeinate (PID ${CAFFEINATE_PID})" | 

| | return 0 | | | } |

|  | stop_caffeinate() { | 
|  | if [[ -z "${CAFFEINATE_PID}" ]]; then | 

| | return 0 | | | fi | | | local pid="${CAFFEINATE_PID}" | | | # | | | # Clear state immediately so cleanup remains idempotent. | | | # | | | CAFFEINATE_PID="" | | | if ! process_is_alive "${pid}"; then | | | debug "caffeinate PID ${pid} is no longer running" | | | wait "${pid}" 2>/dev/null || true | | | return 0 | | | fi | | | debug "sending TERM to caffeinate PID ${pid}" | | | if ! kill -TERM "${pid}" 2>/dev/null; then | | | error "could not terminate caffeinate PID ${pid}" | | | return 1 | | | fi | | | wait "${pid}" 2>/dev/null || true | | | log "Stopped caffeinate" | | | return 0 | | | } | | | # ----------------------------------------------------------------------------- | | | # Watcher cleanup / signals |

|  | # ----------------------------------------------------------------------------- | 
|  | cleanup_watcher() { | 

| | local original_status=$? | | | debug "cleaning up" | | | stop_caffeinate || true | | | if [[ "${IS_DAEMON}" == "1" ]]; then | | | remove_pid_file_if_owned "$$" | | | fi | | | return "${original_status}" | | | } | | | handle_signal() { | | | local signal_name="$1" | | | local exit_code=1 | | | case "${signal_name}" in | | | INT) | | | exit_code=130 | | | ;; | | | TERM) | | | exit_code=143 | | | ;; | | | HUP) | | | exit_code=129 | | | ;; | | | esac |

|  | log "Received ${signal_name}; shutting down" | 
|  | exit "${exit_code}" | 

| | } | | | # ----------------------------------------------------------------------------- | | | # Argument parsing |

|  | # ----------------------------------------------------------------------------- | 
|  | parse_run_arguments() { | 

| | local custom_apps=false | | | local custom_caffeinate_args=false | | | while (( $# > 0 )); do | | | case "$1" in |

|  | -a\|--app) | 
|  | if (( $# < 2 )); then | 

| | die "$1 requires an application/process name" 2 | | | fi |

|  | if [[ "${custom_apps}" == false ]]; then | 
|  | TARGET_APPS=() | 

| | custom_apps=true | | | fi | | | TARGET_APPS+=("$2") | | | shift 2 |

|  | ;; | 
|  | --app=*) | 
|  | if [[ "${custom_apps}" == false ]]; then | 
|  | TARGET_APPS=() | 

| | custom_apps=true | | | fi | | | TARGET_APPS+=("${1#*=}") | | | shift |

|  | ;; | 
|  | -i\|--interval) | 
|  | if (( $# < 2 )); then | 

| | die "$1 requires a number of seconds" 2 | | | fi | | | POLL_INTERVAL="$2" | | | shift 2 |

|  | ;; | 
|  | --interval=*) | 
|  | POLL_INTERVAL="${1#*=}" | 

| | shift |

|  | ;; | 
|  | -m\|--match) | 
|  | if (( $# < 2 )); then | 

| | die "$1 requires either 'exact' or 'contains'" 2 | | | fi | | | MATCH_MODE="$2" | | | shift 2 |

|  | ;; | 
|  | --match=*) | 
|  | MATCH_MODE="${1#*=}" | 

| | shift |

|  | ;; | 
|  | -c\|--caffeinate) | 
|  | if (( $# < 2 )); then | 

| | die "$1 requires a caffeinate argument" 2 | | | fi |

|  | if [[ "${custom_caffeinate_args}" == false ]]; then | 
|  | CAFFEINATE_ARGS=() | 

| | custom_caffeinate_args=true | | | fi | | | CAFFEINATE_ARGS+=("$2") | | | shift 2 |

|  | ;; | 
|  | --caffeinate=*) | 
|  | if [[ "${custom_caffeinate_args}" == false ]]; then | 
|  | CAFFEINATE_ARGS=() | 

| | custom_caffeinate_args=true | | | fi | | | CAFFEINATE_ARGS+=("${1#*=}") | | | shift |

|  | ;; | 
|  | -v\|--verbose) | 

| | VERBOSE=true | | | shift |

|  | ;; | 
|  | --dry-run) | 

| | DRY_RUN=true | | | shift |

|  | ;; | 
|  | -h\|--help) | 

| | usage | | | exit 0 |

|  | ;; | 
|  | --) | 

| | shift | | | break |

|  | ;; | 
|  | -*) | 

| | die "unknown option: $1" 2 | | | ;; | | | *) | | | die "unexpected argument: $1" 2 | | | ;; | | | esac | | | done | | | validate_positive_integer "${POLL_INTERVAL}" "poll interval" | | | case "${MATCH_MODE}" in | | | exact|contains) | | | ;; | | | *) | | | die \ | | | "match mode must be 'exact' or 'contains'; received: ${MATCH_MODE}" \ | | | 2 | | | ;; | | | esac | | | if (( ${#TARGET_APPS[@]} == 0 )); then | | | die "at least one application/process must be specified" 2 | | | fi | | | local app |

|  | for app in "${TARGET_APPS[@]}"; do | 
|  | if [[ -z "${app}" ]]; then | 

| | die "application/process names may not be empty" 2 | | | fi | | | done | | | if (( ${#CAFFEINATE_ARGS[@]} == 0 )); then | | | die "at least one caffeinate argument must be specified" 2 | | | fi | | | local caffeinate_arg |

|  | for caffeinate_arg in "${CAFFEINATE_ARGS[@]}"; do | 
|  | if [[ -z "${caffeinate_arg}" ]]; then | 

| | die "caffeinate arguments may not be empty" 2 | | | fi | | | done | | | } | | | # ----------------------------------------------------------------------------- | | | # Foreground watcher |

|  | # ----------------------------------------------------------------------------- | 
|  | run_monitor() { | 

| | parse_run_arguments "$@" | | | validate_runtime_dependencies | | | trap cleanup_watcher EXIT | | | trap 'handle_signal INT' INT | | | trap 'handle_signal TERM' TERM | | | trap 'handle_signal HUP' HUP | | | if [[ "${IS_DAEMON}" == "1" ]]; then | | | write_daemon_pid "$$" | | | fi |

|  | log "Watching for: ${(j:, :)TARGET_APPS}" | 
|  | log "Poll interval: ${POLL_INTERVAL}s" | 
|  | log "Process matching: ${MATCH_MODE}" | 
|  | log "Caffeinate arguments: ${(j: :)CAFFEINATE_ARGS}" | 
|  | if [[ "${DRY_RUN}" == true ]]; then | 

| | print_status "Dry-run mode enabled" | | | fi | | | local previous_target="" | | | while true; do | | | local running_target="" |

|  | if running_target="$(find_running_target)"; then | 
|  | debug "detected target process: ${running_target}" | 
|  | if [[ "${running_target}" != "${previous_target}" ]]; then | 
|  | if [[ "${DRY_RUN}" == true ]]; then | 

| | print_status \ | | | "Dry run: would start caffeinate for ${running_target}" | | | else | | | log "Detected ${running_target}" | | | fi | | | previous_target="${running_target}" | | | fi | | | if [[ "${DRY_RUN}" != true ]]; then | | | if ! caffeinate_is_running; then | | | # | | | # If a PID was recorded but is no longer alive, caffeinate died | | | # unexpectedly rather than being intentionally stopped. | | | # | | | if [[ -n "${CAFFEINATE_PID}" ]]; then | | | log \ | | | "caffeinate exited unexpectedly; restarting" | | | fi | | | if ! start_caffeinate; then | | | error \ | | | "will retry caffeinate on the next polling cycle" | | | fi | | | fi | | | fi | | | else |

|  | if [[ -n "${previous_target}" ]]; then | 
|  | if [[ "${DRY_RUN}" == true ]]; then | 

| | print_status "Dry run: would stop caffeinate" | | | else | | | log "No monitored applications are running" | | | fi | | | previous_target="" | | | fi | | | if [[ "${DRY_RUN}" != true ]]; then | | | if caffeinate_is_running; then | | | stop_caffeinate || true | | | elif [[ -n "${CAFFEINATE_PID}" ]]; then | | | debug \ | | | "clearing stale caffeinate PID ${CAFFEINATE_PID}" | | | CAFFEINATE_PID="" | | | fi | | | fi | | | fi | | | sleep "${POLL_INTERVAL}" | | | done | | | } | | | # ----------------------------------------------------------------------------- | | | # Child argument construction |

|  | # ----------------------------------------------------------------------------- | 
|  | build_child_arguments() { | 

| | local app |

|  | for app in "${TARGET_APPS[@]}"; do | 
|  | print -r -- "--app" | 
|  | print -r -- "${app}" | 

| | done |

|  | print -r -- "--interval" | 
|  | print -r -- "${POLL_INTERVAL}" | 
|  | print -r -- "--match" | 
|  | print -r -- "${MATCH_MODE}" | 

| | local caffeinate_arg |

|  | for caffeinate_arg in "${CAFFEINATE_ARGS[@]}"; do | 
|  | print -r -- "--caffeinate=${caffeinate_arg}" | 

| | done |

|  | if [[ "${VERBOSE}" == true ]]; then | 
|  | print -r -- "--verbose" | 

| | fi | | | } | | | # ----------------------------------------------------------------------------- | | | # Background daemon start |

|  | # ----------------------------------------------------------------------------- | 
|  | start_daemon_current_config() { | 

| | validate_runtime_dependencies | | | require_command nohup | | | if [[ "${DRY_RUN}" == true ]]; then | | | die "--dry-run is only supported with the run command" 2 | | | fi | | | ensure_state_dir | | | local existing_pid | | | if daemon_is_running; then | | | existing_pid="$(read_daemon_pid)" | | | print_status \ | | | "caffeinate-ai is already running (PID ${existing_pid})" | | | return 0 | | | fi |

|  | rm -f "${PID_FILE}" | 
|  | local child_args=( | 
|  | "${(@f)$(build_child_arguments)}" | 

| | ) | | | debug "starting detached watcher" | | | # | | | # Verbose logs are deliberately session-scoped rather than append-only. | | | # |

|  | if [[ "${VERBOSE}" == true ]]; then | 
|  | if ! : >"${LOG_FILE}"; then | 
|  | die "could not initialize verbose log: ${LOG_FILE}" | 

| | fi | | | CAFFEINATE_AI_DAEMON=1 \ |

|  | nohup "${SCRIPT_PATH}" run "${child_args[@]}" \ | 
|  | >>"${LOG_FILE}" \ | 

| | 2>&1 \ | | | </dev/null & | | | else | | | CAFFEINATE_AI_DAEMON=1 \ | | | nohup "${SCRIPT_PATH}" run "${child_args[@]}" \ | | | >/dev/null \ | | | 2>&1 \ | | | </dev/null & | | | fi | | | local pid=$! | | | # | | | # Wait for the child to finish initialization and write its PID file. | | | # | | | local attempts=30 | | | local daemon_pid="" |

|  | while (( attempts > 0 )); do | 
|  | if ! process_is_alive "${pid}"; then | 

| | error "caffeinate-ai failed to start" |

|  | if [[ "${VERBOSE}" == true ]]; then | 
|  | error "check log: ${LOG_FILE}" | 

| | fi | | | return 1 | | | fi |

|  | if daemon_pid="$(read_daemon_pid 2>/dev/null)"; then | 
|  | if [[ "${daemon_pid}" == "${pid}" ]] && | 
|  | process_matches_daemon "${pid}"; then | 

| | print_status \ |

|  | "Started caffeinate-ai (PID ${pid})" | 
|  | if [[ "${VERBOSE}" == true ]]; then | 

| | print_status \ | | | "Verbose log: ${LOG_FILE}" | | | fi | | | return 0 | | | fi | | | fi | | | sleep 0.1 | | | (( attempts-- )) | | | done | | | # | | | # Never intentionally leave an unmanageable daemon behind. | | | # |

|  | if process_matches_daemon "${pid}"; then | 
|  | kill -TERM "${pid}" 2>/dev/null \|\| true | 

| | fi | | | rm -f "${PID_FILE}" | | | error "timed out waiting for caffeinate-ai to initialize" |

|  | if [[ "${VERBOSE}" == true ]]; then | 
|  | error "check log: ${LOG_FILE}" | 

| | fi | | | return 1 | | | } | | | # ----------------------------------------------------------------------------- | | | # Background daemon stop |

|  | # ----------------------------------------------------------------------------- | 
|  | stop_daemon_current() { | 

| | validate_lifecycle_dependencies | | | local pid | | | if ! daemon_is_running; then | | | rm -f "${PID_FILE}" | | | print_status \ | | | "caffeinate-ai background watcher is not running" | | | return 0 | | | fi | | | pid="$(read_daemon_pid)" | | | print_status \ |

|  | "Stopping caffeinate-ai (PID ${pid})" | 
|  | if ! kill -TERM "${pid}" 2>/dev/null; then | 

| | error "could not send TERM to PID ${pid}" | | | return 1 | | | fi | | | # | | | # Allow roughly five seconds for graceful shutdown. | | | # | | | local attempts=50 |

|  | while (( attempts > 0 )); do | 
|  | if ! process_matches_daemon "${pid}"; then | 

| | break | | | fi | | | sleep 0.1 | | | (( attempts-- )) | | | done | | | if process_matches_daemon "${pid}"; then | | | error \ | | | "process did not stop gracefully; sending SIGKILL" | | | kill -KILL "${pid}" 2>/dev/null || true | | | attempts=20 |

|  | while (( attempts > 0 )); do | 
|  | if ! process_matches_daemon "${pid}"; then | 

| | break | | | fi | | | sleep 0.1 | | | (( attempts-- )) | | | done | | | fi |

|  | if process_matches_daemon "${pid}"; then | 
|  | error "could not stop caffeinate-ai PID ${pid}" | 

| | return 1 | | | fi | | | remove_pid_file_if_owned "${pid}" | | | print_status "Stopped caffeinate-ai" | | | return 0 | | | } | | | # ----------------------------------------------------------------------------- | | | # Restart / toggle |

|  | # ----------------------------------------------------------------------------- | 
|  | restart_daemon_current_config() { | 

| | stop_daemon_current || return $? | | | start_daemon_current_config | | | } | | | toggle_daemon_current_config() { | | | if daemon_is_running; then | | | stop_daemon_current | | | else | | | start_daemon_current_config | | | fi | | | } | | | # ----------------------------------------------------------------------------- | | | # caffeinate child lookup |

|  | # ----------------------------------------------------------------------------- | 
|  | get_caffeinate_child_pids() { | 

| | local daemon_pid="$1" | | | pgrep \ |

|  | -U "${UID}" \ | 
|  | -P "${daemon_pid}" \ | 

| | -x \ | | | caffeinate \ | | | 2>/dev/null || | | | true | | | } | | | get_first_caffeinate_child_pid() { | | | local daemon_pid="$1" | | | local raw_pids |

|  | raw_pids="$(get_caffeinate_child_pids "${daemon_pid}")" | 
|  | if [[ -z "${raw_pids}" ]]; then | 

| | return 1 | | | fi |

|  | local pids=( | 
|  | "${(@f)raw_pids}" | 

| | ) | | | if (( ${#pids[@]} == 0 )); then | | | return 1 | | | fi | | | print -r -- "${pids[1]}" | | | } | | | # ----------------------------------------------------------------------------- | | | # Status |

|  | # ----------------------------------------------------------------------------- | 
|  | status_daemon() { | 

| | validate_lifecycle_dependencies | | | local pid | | | if ! daemon_is_running; then | | | rm -f "${PID_FILE}" | | | print_status \ | | | "caffeinate-ai background watcher is not running" | | | return 1 | | | fi | | | pid="$(read_daemon_pid)" | | | print_status \ | | | "caffeinate-ai background watcher is running (PID ${pid})" | | | local caffeinate_pid="" | | | if caffeinate_pid="$( | | | get_first_caffeinate_child_pid "${pid}" | | | )"; then | | | print_status \ | | | "caffeinate is active (PID ${caffeinate_pid})" | | | else | | | print_status \ | | | "caffeinate is currently inactive" | | | fi | | | return 0 | | | } | | | # ----------------------------------------------------------------------------- | | | # Doctor |

|  | # ----------------------------------------------------------------------------- | 
|  | doctor() { | 

| | parse_run_arguments "$@" | | | validate_doctor_dependencies | | | if [[ "${DRY_RUN}" == true ]]; then | | | die "--dry-run is only supported with the run command" 2 | | | fi | | | print_status "caffeinate-ai diagnostics" | | | print_status "" | | | # | | | # Watcher | | | # | | | print_status "Watcher:" | | | local daemon_pid="" | | | if daemon_is_running; then | | | daemon_pid="$(read_daemon_pid)" | | | print_status \ | | | " background: running (PID ${daemon_pid})" | | | else | | | print_status \ | | | " background: not running" | | | fi | | | print_status "" | | | # | | | # Targets | | | # | | | print_status "Targets:" | | | local app | | | local raw_pids |

|  | for app in "${TARGET_APPS[@]}"; do | 
|  | raw_pids="$(get_matching_pids "${app}" 2>/dev/null \|\| true)" | 
|  | if [[ -n "${raw_pids}" ]]; then | 
|  | local pids=( | 
|  | "${(@f)raw_pids}" | 

| | ) | | | print_status \ | | | " ${app}: running (PIDs ${(j:, :)pids})" | | | else | | | print_status \ | | | " ${app}: not running" | | | fi | | | done | | | print_status "" | | | # | | | # caffeinate | | | # | | | print_status "Sleep prevention:" | | | local caffeinate_pid="" |

|  | if [[ -n "${daemon_pid}" ]] && | 
|  | caffeinate_pid="$( | 

| | get_first_caffeinate_child_pid "${daemon_pid}" | | | )"; then | | | print_status \ | | | " caffeinate: active (PID ${caffeinate_pid})" | | | local assertions |

|  | assertions="$(pmset -g assertions 2>/dev/null \|\| true)" | 
|  | if print -r -- "${assertions}" \| | 

| | grep -E \ |

|  | "pid[[:space:]]+${caffeinate_pid}\\(" \ | 
|  | >/dev/null 2>&1; then | 

| | print_status \ | | | " power assertion: present" | | | else | | | print_status \ | | | " power assertion: not detected" | | | fi | | | else | | | print_status \ | | | " caffeinate: inactive" | | | print_status \ | | | " power assertion: none expected" | | | fi | | | print_status "" | | | # | | | # Effective diagnostic configuration | | | # | | | print_status "Configuration:" | | | print_status \ | | | " interval: ${POLL_INTERVAL}s" | | | print_status \ | | | " match: ${MATCH_MODE}" | | | print_status \ | | | " apps: ${(j:, :)TARGET_APPS}" | | | print_status \ | | | " caffeinate arguments: ${(j: :)CAFFEINATE_ARGS}" | | | return 0 | | | } | | | # ----------------------------------------------------------------------------- | | | # Background configuration preparation |

|  | # ----------------------------------------------------------------------------- | 
|  | prepare_background_configuration() { | 

| | parse_run_arguments "$@" | | | if [[ "${DRY_RUN}" == true ]]; then | | | die "--dry-run is only supported with the run command" 2 | | | fi | | | validate_runtime_dependencies | | | require_command nohup | | | } | | | # ----------------------------------------------------------------------------- | | | # Main |

|  | # ----------------------------------------------------------------------------- | 
|  | main() { | 
|  | local subcommand="${1:-run}" | 
|  | case "${subcommand}" in | 

| | start) | | | shift | | | prepare_background_configuration "$@" | | | with_operation_lock \ | | | start_daemon_current_config | | | ;; | | | stop) | | | shift | | | if (( $# > 0 )); then | | | die "stop does not accept additional arguments" 2 | | | fi | | | validate_lifecycle_dependencies | | | with_operation_lock \ | | | stop_daemon_current | | | ;; | | | restart) | | | shift | | | # | | | # Validate everything before stopping an existing daemon. That prevents | | | # an invalid replacement configuration from unnecessarily taking down | | | # a working watcher. | | | # | | | prepare_background_configuration "$@" | | | with_operation_lock \ | | | restart_daemon_current_config | | | ;; | | | toggle) | | | shift | | | prepare_background_configuration "$@" | | | with_operation_lock \ | | | toggle_daemon_current_config | | | ;; | | | status) | | | shift | | | if (( $# > 0 )); then | | | die "status does not accept additional arguments" 2 | | | fi | | | status_daemon | | | ;; | | | doctor) | | | shift | | | doctor "$@" | | | ;; | | | run) | | | # "run" may be implicit when no arguments were supplied. | | | if (( $# > 0 )); then | | | shift | | | fi | | | run_monitor "$@" | | | ;; | | | help) | | | usage |

|  | ;; | 
|  | -h\|--help) | 

| | usage |

|  | ;; | 
|  | -*) | 

| | # | | | # Shorthand: | | | # | | | # caffeinate-ai --verbose | | | # | | | # is equivalent to: | | | # | | | # caffeinate-ai run --verbose | | | # | | | run_monitor "$@" | | | ;; | | | *) | | | error "unknown command: ${subcommand}" | | | print >&2 | | | usage >&2 | | | exit 2 | | | ;; | | | esac | | | } | | | main "$@" |


  1. 0-9 ↩︎

── more in #ai-tools 4 stories · sorted by recency
── more on @chatgpt 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/caffeinate-ai-keep-m…] indexed:0 read:35min 2026-10-02 · —