Skip to content

API — ShipTools ​

The ShipTools umbrella package. In phase 1 its public Julia surface is the installer for the sot-comm session-to-session messaging system; the explorer itself runs as the Rust frontend and backend plus the Julia kernel and plugins, which are documented under Design & Internals and Extending Ship of Tools.

ShipTools.CODEX_HOME_PROFILES Constant

Login profile files that may export CODEX_HOME, checked in this order (a later file's assignment wins, matching shell layering).

source
ShipTools._add_comm_hook! Method
julia
_add_comm_hook!(event, script, settings)

Idempotently add a Claude Code hook for event ("UserPromptSubmit", "Notification", "Stop", …) running ~/.sot-comm/bin/<script> to settings, preserving all existing config. Uses jq so the merge is structural, not a clobbering rewrite. Falls back to printing the exact JSON to add by hand when the file can't be parsed — never overwrites a file it could not safely read. Returns false when it warned and left the file as it was, true otherwise (already present, added).

A MISSING settings.json (e.g. a fresh ~/.claude that has never been run yet) gets a fresh {} created first, so the install still gets the hook registration.

settings must be the RESOLVED file ([_claude_settings_targets]), never a symlink: the publish is a rename, which replaces a link at the destination with a plain file instead of writing through it.

source
ShipTools._check_installed Method
julia
_check_installed(dstdir, files; executable = Returns(false)) -> Vector{String}

Check that every name in files exists in dstdir, and — for names where executable(name) is true — that the destination copy is actually executable. Returns one description per problem found (empty on success); never throws, so callers (_install_files) can fold it into a single combined report alongside their own failures.

install_file already guarantees a single failed copy can't silently destroy a working file, but nothing so far confirms every source file actually landed — a copy that returns without error yet produces nothing is a different failure mode (unproven on this platform, not ruled out on others). This is that confirmation.

source
ShipTools._claude_config_dirs Method
julia
_claude_config_dirs(home, claude_dir) -> Vector{String}

Every Claude configuration folder an account uses, each once: claude_dir (the install's own), the default home/.claude, and every home/.claude-auth/<name> folder whose name passes [_is_account_name]. A folder that does not exist is included (callers test what they need).

source
ShipTools._claude_settings_targets Method
julia
_claude_settings_targets(home, claude_dir; unhooked = String[]) -> Vector{String}

Every Claude account's settings.json the comm hooks go into, as the REAL files to write, each once, in this order: claude_dir (the install's own dir, claude_home()), the default home/.claude, then every home/.claude-auth/<name> folder whose name passes [_is_account_name] (sorted). Links are resolved (realpath), so a writer renames onto the target and a link stays a link, and two accounts linked to one file are one entry.

A MISSING settings.json is returned (unresolved) for exactly one dir, the create dir: claude_dir, unless that is home/.claude-auth or lies in it (compared by real location, so a relative or linked CLAUDE_CONFIG_DIR is caught), in which case home/.claude (the backend's link then carries that file into the account), where _add_comm_hook! creates it fresh. No file is ever created under .claude-auth: a real file there would permanently shadow the shared settings.json the daemon links in (or copies in) at spawn (ensure_account_links leaves any existing entry alone). Any other dir without one is skipped. A dangling link, or a path that resolves to something other than a regular file, is skipped with a warning and never created through. Every skipped folder or file is also pushed onto unhooked with its reason.

source
ShipTools._codex_home_profile_mismatch Method
julia
_codex_home_profile_mismatch(installed_home)

Side-effecting wrapper around _parse_codex_home_export: reads whichever of CODEX_HOME_PROFILES exist under homedir() and reports the last assignment found across them, if any, and if it differs from installed_home. Returns nothing when no profile assigns CODEX_HOME, or the assignment matches what was just installed into. Otherwise returns (value, raw, file) — value is nothing when the assignment couldn't be expanded with confidence, in which case the caller warns quoting raw.

source
ShipTools._install_claude_hooks Method
julia
_install_claude_hooks(claude_dir; unhooked)

Idempotently register the work-state hooks in every Claude account's settings.json: each real file [_claude_settings_targets] returns for claude_dir (the install's own dir, the default ~/.claude, and each ~/.claude-auth/<name> that has one), links resolved, each written once. The hook scripts themselves install with the other bin folders (_comm_bin_files), whatever clis; this function only wires them.

The work-state hooks make state event-driven — instant, automatic, and free of model cooperation: UserPromptSubmit → working, PreToolUse on AskUserQuestion → blocked, Stop → idle, and a PostToolUse heartbeat. They replace the pane-scraping heuristic, which could not be instant (a poll), was fooled by an agent's own output, and could never tell "blocked on the user" from "idle".

Each settings.json edit is a non-destructive jq merge (via [_add_comm_hook!]): it adds one entry for that event only if absent and preserves every other hook (repo-boundary-guard, tmux-send-guard, … are untouched). A missing settings.json is created fresh in one dir only, the create dir of [_claude_settings_targets], and never under ~/.claude-auth; an unparseable one is left alone and the exact JSON to add by hand is printed. jq is on PATH here: install_comm stops before any adapter without it.

source
ShipTools._install_files Method
julia
_install_files(srcdir, dstdir, files; executable = Returns(false), text = nothing, rename)

Install files (names found under srcdir, may include subdirectory components) into dstdir one at a time via install_file — continuing past a single failure, so ONE destination a live process still has open (comm-relay.sh, observed live) cannot block updating the rest of a directory. executable(name) files get chmod(0o755) after a successful install. text(name), when given, is what name installs as in place of its source's bytes (_comm_bin_text); rename goes to install_file.

Raises ONE error at the end combining every file that could not be updated (old copy kept, if one existed — stale is an acceptable outcome; MISSING is not) with anything _check_installed still finds wrong. Every reason lands in one joined, single-line string, because a launcher that truncates a crash's stderr to its tail still needs the useful part to survive.

source
ShipTools._install_launchers Method
julia
_install_launchers(srcbin)

Install bare-command launcher scripts (e.g. ccb) from srcbin into ~/.local/bin, making each executable, then prune COMM_DEPRECATED_LAUNCHERS from the same dir. No-op if srcbin is absent. ~/.local/bin is the conventional user PATH dir; a warning fires if it is not on PATH so bare commands won't resolve.

source
ShipTools._install_skills Method
julia
_install_skills(srcdir, skillsroot) -> Vector{String}

Install every skill directory under srcdir (one containing a SKILL.md, and keep(name)) into skillsroot, file by file through install_file's atomic copy-then-rename, plus one orphan sweep. Every file is either the old copy or the new one — never missing, never partial. The destination is NEVER removed: rename(2) cannot replace a non-empty directory, so a "directory swap" is really rename-aside-then-rename-in, which re-creates the very window it claims to close.

One path for BOTH adapters. Returns the names that landed ("/name"); raises one combined error naming every skill that kept its previous copy, which the caller records as a stage failure.

source
ShipTools._jq_install_command Function
julia
_jq_install_command(kernel = Sys.KERNEL)

The command that installs jq on the OS kernel names (Sys.KERNEL): winget on Windows, Homebrew on macOS, apt elsewhere. install_comm names it when it stops without jq; scripts/install.sh names the macOS and Linux ones.

source
ShipTools._legacy_skill_hashes Method
julia
_legacy_skill_hashes() -> Dict{String,Set{String}}

The skills an earlier installer placed straight into a Claude skills folder: each name with the hash of every version the repo ever shipped (comm/legacy-skills.txt, made by scripts/legacy-skill-hashes.sh; frozen history).

source
ShipTools._parse_codex_home_export Method
julia
_parse_codex_home_export(text; user, home) -> (value, raw)

Pure, side-effect-free: scans one profile file's TEXT for the LAST export CODEX_HOME=... or CODEX_HOME=...; export CODEX_HOME assignment (a commented-out line is ignored) and expands it via _expand_codex_home_value.

Returns (value, raw):

  • no assignment anywhere in text -> (nothing, nothing)

  • assignment found, expands cleanly -> (path, raw_value)

  • assignment found, can't expand it -> (nothing, raw_value) — callers quote raw_value in their warning rather than dropping the case silently.

Takes user/home as plain arguments — it never reads ENV or homedir() itself — so it is testable without touching the real environment.

source
ShipTools._prune_layer_skills! Method
julia
_prune_layer_skills!(layerskills, srcdirs)

The layer is the installer's alone, so a skill directory in it that no source folder in srcdirs ships any more, or that is an onboarding skill ([CLAUDE_GLOBAL_SKILLS], which live in the global folder), goes: a retired skill must not stay loadable in a row. Called only after every skill installed.

source
ShipTools._reap_markers Method
julia
_reap_markers(dst)

Remove this destination's own leftover markers, run only AFTER a successful rename onto dst. A directory-wide PRE-pass can delete another run's aside during the window between its two renames — the one moment that aside is the only copy of the old file; done after our own rename succeeds, the worst case is that the other run's restore fails and dst keeps our valid new content, never missing. An aside goes unconditionally (best effort). A staging file goes ONLY when its host field is this host AND its pid is not alive: a live pid means a run is writing it right now, and another host's pid number says nothing about liveness here, so both exclusions are absolute.

source
ShipTools._remove_stale_comm_hooks! Method
julia
_remove_stale_comm_hooks!(settings)

Strip every comm command that is not part of the current wiring (_COMM_STATE_HOOKS, matched by event, command and matcher) from settings (a resolved file, see [_add_comm_hook!]) — retired scripts (any other ~/.sot-comm/bin/comm-status-*.sh, a comm-subagent-guard.sh on another event or matcher, the retired comm-postcompact-reminder.sh / comm-postclear-reminder.sh) and a current script wired to an old event or matcher (notably the old Notification→blocked that lit agents red on plain idle). Commands are removed one at a time, a group is dropped only when left empty and an event only when it has no groups, so every non-comm hook is preserved, including a user's own script whose name merely resembles a comm one (only commands under .sot-comm/bin/ are comm commands), even one sharing a group with a comm command. A current hook is left in place, so a re-run rewrites nothing. No-op if settings.json is absent/unparseable (a fresh ~/.claude has nothing to prune yet either way). Returns false when the file could not be pruned (an error), true otherwise.

source
ShipTools._repo_commit Method

Short git commit this checkout is at (dirty-suffixed if the working tree has uncommitted changes), or "unknown" when git is unavailable.

source
ShipTools._retire_placed_skills! Method
julia
_retire_placed_skills!(skillsroot; known = _legacy_skill_hashes())

Remove from a Claude skills folder the skills an earlier install put there, and nothing else. A folder goes only when its contents hash to a version the repo shipped of the skill of that name; any other folder of such a name is a person's own and stays, with a warning. A skills folder that is itself a link is never acted through (its target is somebody's dotfiles), and a link inside it is never removed. The rule holds on every run, so it keeps no marker.

source
ShipTools._skill_folder_hash Method
julia
_skill_folder_hash(dir) -> Union{Nothing,String}

The hash scripts/legacy-skill-hashes.sh gives a skill folder: sha256 of its sorted manifest, one line <path> <git blob sha1> per file. nothing for a folder with no file or with any link in it: that is not a copy an installer made.

source
ShipTools._stage! Method
julia
_stage!([f], problems, label)

Run f, and on a throw record ONE line on problems naming label and the cause, then return. A failure is recorded and named, never propagated: no file strands its skill, no skill strands the skills after it, no stage strands the next one, no adapter strands the next one. The run raises one combined error at the end instead.

source
ShipTools._sweep_orphans! Method
julia
_sweep_orphans!(skilldst, shipped)

Delete files under an installed skill that the source no longer ships — the only thing the old rm(dst; recursive = true) ever bought: a renamed or deleted reference must not linger and be read as current. Names carrying install_file's in-flight or aside markers are NOT ours to delete here (another run may be mid-swap on them; reaping lives in install_file, which knows their owner). Emptied directories go too. Best effort: a file a live process holds open warns, it does not fail the skill — the skill itself is current. Called ONLY for a skill whose install raised nothing, since sweeping a failed skill could delete a resource while its SKILL.md stayed old.

source
ShipTools.claude_home Method

Resolved runtime home for the DEFAULT claude account (honors $CLAUDE_CONFIG_DIR).

source
ShipTools.claude_layer Method

The Claude layer folder, under the comm home.

source
ShipTools.claude_layer_skills Method

The folder the Claude layer's skills install into.

source
ShipTools.codex_home Method

Resolved runtime home for codex (honors $CODEX_HOME).

source
ShipTools.comm_home Method

Resolved runtime home for sot-comm (honors $SOT_COMM_HOME).

source
ShipTools.install_comm Method
julia
install_comm(; clis = [:claude])

Install sot-comm, creating the comm folder owner-only (0700) when it does not exist. Installs the files of every folder listed in comm/bin-folders.txt into $SOT_COMM_HOME/bin (default ~/.sot-comm/bin), each part inside the file that sources it (_comm_bin_text), whatever clis, and installs the adapter for each CLI in clis (:claude skills into the Claude layer, the mail-check hooks, and the retirement of what an earlier install put in the user's own folders; :codex skills/hooks/plugin). Idempotent — safe to re-run to update an existing install. Without jq on PATH it stops before it changes anything, with one line naming the command that installs jq (_jq_install_command).

source
ShipTools.install_file Method
julia
install_file(src, dst; rename = Base.Filesystem.rename, text = nothing)

Copy src to a temporary name beside dst, then rename it onto dst, so dst is never partially written. With text, the temporary then holds text() instead of src's bytes, keeping src's mode. When the rename is refused because a running process holds dst open (Windows), the old file is moved aside under a .stale- name rather than deleted — the process keeps its inode — the new file lands, and the aside copy is reaped by the next successful replace of that name. Any failure removes the temporary and throws; if the second rename fails the old file is put back, so dst is never missing.

source
ShipTools.update_comm Method
julia
update_comm(; clis = [:claude, :codex])

Re-sync an existing install from the repo source. Alias of [install_comm] (install is idempotent); run after git pull on each machine to close version skew.

source

An agentic development environment for Julia. Released under the AGPL-3.0-or-later, with a commercial license on request.