← Documents
Reference · ttran · Textfile Transfer

ttran

ttranTextfile Transfer, machine-to-machine sharing. A 4-char-ID drop-box on ALPUCA. Any machine that can SSH to ALPUCA can put a file and get back a short ID like AA03; any Claude session on any other machine can cat/get it back. Use it to hand prompts, scratch results, and pasted context between sessions without going through git. (Binaries work too — cat is text-only, but put/get handle any file.)

Prepared 2026-05-06 · Sponic Gardens · Wrapper at infra/bin/ttran in the sponic repo
Live Bash, no deps SSH/SCP, host-agnostic
Contents
  1. Why this exists
  2. Mechanics — how it works under the hood
  3. Commands — the wrapper API
  4. Setup on a new machine
  5. Day-to-day usage examples
  6. Worked example — two Claude sessions on two Macs
  7. What NOT to put in ttran
  8. Failure modes & diagnosis
  9. Internals reference

Why this exists

Two Claude sessions on two Macs can't see each other's filesystem, terminal, or context. Before ttran, sharing a file or a long pasted context across machines meant one of:

ttran is the lightweight middle ground. The user types "put this in ttran" in one session, gets back a 4-char ID like AA03, then types "grab the ttran doc AA03" in another session on another machine. That's it — no git, no API, no service account.

Design constraint that drove everything else. The IDs are deliberately 4 characters so a human can dictate them, type them on a phone, or remember them for 30 seconds. AA03 is short enough to read out loud; a UUID would not be.

Mechanics — how it works under the hood

Storage

The channel is a single directory: /Volumes/PortoSams2T/ttran/ on ALPUCA. PortoSams2T is a Samsung 2TB Portable SSD plugged into the always-on Mac. Every put writes one file there; every get / cat / rm resolves an ID back to a file in that same directory.

There is no DB, no index, no metadata file. The directory listing is the database. Filenames carry the ID prefix:

$ ssh alpuca@alpuca 'ls /Volumes/PortoSams2T/ttran/'
AA01 m4-session-results.txt
AA02 m4-fix-prompt.md
AA03 notes.md
.lock.d/                  # mkdir-based lock dir, only present during alloc

The literal space between the ID and the rest of the name is load-bearing — the wrapper uses grep -E '^AA03 ' to resolve an ID back to a filename.

Transport

Every operation on a client machine is one (or two) SSH/SCP calls to alpuca@alpuca:

What ttran actually requires is just three things: a host the client can reach over SSH, a working SSH key authorized for that host, and a writable directory at $TTRAN_DIR. Everything else is a deployment choice. Sponic uses Tailscale to make ALPUCA reachable as alpuca@alpuca from any of our Macs without exposing SSH to the public internet — MagicDNS resolves the name on every tailnet member, the IP can change without breaking anything, and there's no port-forward to manage. But the wrapper is host-agnostic: override TTRAN_HOST with anything that works for you (LAN Bonjour [email protected] when you're on the same network, a public IP + port-forward, a Cloudflare Tunnel SSH endpoint, a WireGuard or ZeroTier address, etc.). If ssh "$TTRAN_HOST" 'whoami' returns alpuca, ttran works.

ID allocation

IDs are 4 characters: 2 letters + 2 digits, allocated sequentially:

AA01 → AA02 → ... → AA99 → AB01 → AB02 → ... → AZ99 → BA01 → ... → ZZ99

That's 26 × 26 × 99 = 66,924 slots. At even 100 transfers/day you'd run out in 22 months — for the intended use (scratch handoff between sessions) it's effectively unbounded.

The next-ID computation is the only part of the system that must be atomic across simultaneous puts from two machines. Server-side bash, on the SSH connection:

# Pseudocode of the alloc step (runs on ALPUCA, inside ssh)
mkdir -p /Volumes/PortoSams2T/ttran
LOCKDIR=/Volumes/PortoSams2T/ttran/.lock.d
for _ in $(seq 1 50); do
  if mkdir "$LOCKDIR" 2>/dev/null; then     # atomic on every POSIX fs
    trap 'rmdir "$LOCKDIR"' EXIT
    break
  fi
  sleep 0.1
done
last=$(ls /Volumes/PortoSams2T/ttran | grep -E '^[A-Z]{2}[0-9]{2} ' | awk '{print $1}' | sort | tail -1)
# … bump the digits, carry to the letter pair on overflow, echo the new ID
Why mkdir, not flock? macOS doesn't ship the flock(1) binary — flock is a Linux util. mkdir on a single-name path is atomic on every macOS filesystem (APFS, HFS+) and on every Linux fs we'd ever care about, so mkdir LOCK; trap 'rmdir LOCK' EXIT is the portable equivalent.

What gets stored

Files land at /Volumes/PortoSams2T/ttran/<ID> <basename> — the original basename is preserved (with its extension), prefixed by the ID and a single space. ttran put /path/to/notes.md creates AA03 notes.md. ttran put data.csv "feb sales" creates AA04 feb sales (custom name, no extension auto-added).

There is no compression, no encryption, no checksumming. The drive itself is encrypted at rest by macOS FileVault, but the channel does no application-level crypto — treat it like any folder.

Commands — the wrapper API

The wrapper is infra/bin/ttran in the sponic repo. ~150 lines of bash, zero deps beyond ssh and scp. Symlink it into your PATH (see Setup) and call it as plain ttran.

CommandWhat it doesReturns
ttran put <file> [name] Upload <file> to the channel. If [name] is given, it overrides the basename (including extension — you control the full stored name). AA03 AA03 <name>
ttran get <id> [dest] Download the file with that ID into [dest] dir (default .). Filename in the dest matches the stored name. full path to the downloaded file
ttran cat <id> Stream the file's contents to stdout. Use this for text — it pipes directly into Claude's context without a local temp file. file contents on stdout
ttran ls List everything in the channel, oldest first (ls -lhrt). ls-style line per file
ttran rm <id> Permanently delete the file with that ID. Same semantics as Unix rm — no trash, no recovery. "removed AA03 (AA03 <name>)"
ttran path Print the remote target as user@host:path — useful for one-off scp/rsync outside the wrapper. alpuca@alpuca:/Volumes/PortoSams2T/ttran
ttran help Print the help block. help text
Override host or path via env vars: TTRAN_HOST=user@host TTRAN_DIR=/some/path ttran ls. Useful for testing against a different drive. Defaults are alpuca@alpuca and /Volumes/PortoSams2T/ttran.

Setup on a new machine

Three one-time steps. The whole thing takes < 2 minutes.

1

SSH access to ALPUCA. The default $TTRAN_HOST is alpuca@alpuca, which Sponic resolves via Tailscale MagicDNS — if you're on Sponic's tailnet, install Tailscale and join. If you're not on tailnet, that's fine — just point $TTRAN_HOST at whatever address you can reach ALPUCA on (LAN [email protected], a public IP, etc.). Either way, verify and authorize your key:

# Pick whichever resolves for you:
export TTRAN_HOST=alpuca@alpuca         # Sponic tailnet (default — leave unset)
# export [email protected] # same LAN as ALPUCA, Bonjour
# export TTRAN_HOST=alpuca@<your-host> # any other reachable host

ssh "$TTRAN_HOST" 'whoami'              # should print "alpuca"
# If you need to authorize the key:
ssh-copy-id "$TTRAN_HOST"
# or, if ssh-copy-id is missing:
cat ~/.ssh/id_ed25519.pub | ssh "$TTRAN_HOST" 'cat >> ~/.ssh/authorized_keys'

If ssh fails with Connection closed by … port 22, see Failure modes — the cause is almost always the username gotcha (the user on ALPUCA is alpuca, not your local whoami). If ssh can't even reach the host, you have a network/DNS problem to solve first — ttran can't help with that, but any working ssh path will do.

2

Symlink onto PATH so ttran works as a plain command:

# Apple Silicon: pick a writable bin dir on your PATH.
ln -sf "$(git rev-parse --show-toplevel)/infra/bin/ttran" "$HOME/bin/ttran"
# or, if you'd rather have it in /usr/local/bin (needs sudo on Apple Silicon):
sudo ln -sf "$(git rev-parse --show-toplevel)/infra/bin/ttran" /usr/local/bin/ttran
ttran help

The symlink chases back to whichever sponic checkout you're in — if you have multiple checkouts, point it at your primary one.

3

Append the ttran block to ~/.claude/CLAUDE.md. This is the user-global Claude Code instructions file (loaded into every session on the machine, not just sponic). The block teaches Claude to recognize phrasings like "grab the ttran doc AA03" and run ttran cat AA03 automatically. Copy the block from the sponic repo's root CLAUDE.md "ttran" section, or from infra/runbook.md → "ttran".

If you skip step 3, ttran still works on the command line — you'll just have to spell the exact subcommand to Claude ("run ttran cat AA03") instead of using natural phrasing.

Smoke test

echo "hello from $(hostname -s) at $(date)" > /tmp/ttran-hello.txt
ID=$(ttran put /tmp/ttran-hello.txt | awk '{print $1}')
ttran cat "$ID"
ttran rm  "$ID"

If all three round-trip cleanly, the machine is set up.

Day-to-day usage examples

Sharing a debug log between sessions

# Session A: you have a long stack trace in /tmp/crash.log
$ ttran put /tmp/crash.log
AA05  AA05 crash.log

# Tell session B: "grab the ttran doc AA05"
# Session B (different Mac, different Claude):
$ ttran cat AA05
<…full stack trace, piped into Claude's context…>

Handing a long prompt to a different machine

# Session A drafted a long prompt at /tmp/agent-spec.md
$ ttran put /tmp/agent-spec.md "agent-spec-v3"
AA06  AA06 agent-spec-v3

# Session B (on the machine that will run the agent):
$ ttran get AA06
./AA06 agent-spec-v3
$ mv "AA06 agent-spec-v3" prompt.md  # rename if you like

Quick "what's in there"

$ ttran ls
-rw-r--r--@ 1 alpuca  staff   5.6K May  6 16:39 AA01 m4-session-results.txt
-rw-r--r--  1 alpuca  staff   3.2K May  6 16:40 AA02 m4-fix-prompt.md
-rw-r--r--  1 alpuca  staff    2.1K May  6 22:14 AA05 crash.log

Cleanup when you're done

$ ttran rm AA05
removed AA05 (AA05 crash.log)

Worked example — two Claude sessions on two Macs

The full intended flow. You have a Claude Code session open on the M4 (call it Session A) and another on the M2 (Session B).

Session A · M4

You: "Put /tmp/notes.md in ttran."

Claude runs ttran put /tmp/notes.md and reports:

"Done — AA07 notes.md."

You walk over to the other machine and start a Claude Code session there.

Session B · M2 (different Mac, fresh Claude)

You: "Grab the ttran doc AA07 and use it as the spec for the next step."

Claude runs ttran cat AA07, reads the contents into context, and proceeds.

The ID handoff is the only manual step — Claude on Mac A doesn't talk to Claude on Mac B. You shuttle the four characters across however (paste it, dictate, take a screenshot, retype). That's why the IDs are short.

You can also send results back. If Session B produces output that Session A wants, ask Session B: "give me the ttran ID for that output". Claude on B runs ttran put, returns a fresh ID, and you carry that ID back to Session A and say "grab the ttran doc AA08".

What NOT to put in ttran

Don'tWhyUse instead
Secrets, API keys, raw credentials The drive isn't encrypted at rest beyond macOS FileVault, and the channel has no ACL beyond "you can SSH to alpuca". Anyone who can SSH in can ls and read every file. Bitwarden — collection DevOps-sponicgarden. See infra/runbook.md.
PII, customer data, anything regulated Same reason — no audit trail, no access controls, no retention policy. Supabase tables with RLS, or never leave the originating system.
Anything you want to keep This is scratch. Nothing is backed up. Files live until someone rms them or the drive fills. Commit to the repo, or upload to R2 via the image-gen wrapper for images.
Large binaries (> 500 MB) SSH/SCP moves them just fine, but it's slow over a long-haul connection. The drive is 2 TB so capacity isn't the issue — throughput is. scp straight to RVAULT20 (the 18 TB drive on ALPUCA), or upload to R2.

Failure modes & diagnosis

Connection closed by ... port 22

The most common failure on a new machine. Almost always means the SSH client is sending the wrong remote username. macOS sshd silently drops the connection with a generic "Connection closed" on the wire when PAM rejects the username; the real error (fatal: PAM user mismatch) only appears in the target Mac's unified log.

Fix: the user on ALPUCA is alpuca. Don't rely on your local whoami. Always invoke as ssh alpuca@alpuca (or set User alpuca for Host alpuca in ~/.ssh/config).

Diagnose by SSHing into the target with the right username, then:

/usr/bin/log show --predicate 'process == "sshd-session"' --info --last 10m \
  | grep -iE "fatal|invalid|disconn|preauth" | grep -v libsystem_info

flock: command not found

Shouldn't happen with the current wrapper — we use mkdir-based locking. If you see this, you're running an old copy of ttran from before commit d0960f7. Pull the sponic repo, refresh the symlink target.

ttran: no file with id AA0X

Either the file was already rm'd, or the ID is mistyped. Run ttran ls and confirm the ID exists. IDs are case-sensitive uppercase.

SSH connects but ttran ls hangs or returns no files

The drive is unmounted or the Mac is asleep. ALPUCA should never sleep (it's the always-on host) and PortoSams2T should auto-mount. If ssh "$TTRAN_HOST" 'ls /Volumes/' works but doesn't show PortoSams2T, physically check the drive on ALPUCA — cable, power, Disk Utility.

Channel directory missing on ALPUCA

The wrapper auto-creates /Volumes/PortoSams2T/ttran/ on first put, so this shouldn't happen unless someone removed it manually. Re-running any subcommand recreates it.

ID space exhausted (would only happen at ZZ99)

The wrapper refuses to upload past ZZ99. Run ttran ls and start rm-ing old slots to free room. In practice this should never happen for human-driven use.

Internals reference

ThingValue
Wrapper sourceinfra/bin/ttran in the sponic repo
Server-side path/Volumes/PortoSams2T/ttran/ on ALPUCA
Lock dir (transient)/Volumes/PortoSams2T/ttran/.lock.d/
Filename format<ID> <basename> — literal space separator
ID format[A-Z]{2}[0-9]{2}AA01, AA02, …, ZZ99
ID space26 × 26 × 99 = 66,924 slots
SSH target (default)alpuca@alpuca (Tailscale MagicDNS — Sponic's deployment choice; the wrapper is host-agnostic)
SSH target (override)$TTRAN_HOST
Path (override)$TTRAN_DIR
DriveSamsung 2TB Portable SSD ("PortoSams2T") plugged into ALPUCA
Concurrency primitivemkdir-based lock; up to 5s wait (50 × 0.1s)
CryptoNone at the application layer. Drive is FileVault-encrypted at rest; transport is SSH.
BackupsNone — channel is intentionally scratch.
Onboarding doconboard-new-machine → "ttran" section
Operational recipeinfra/runbook.md → "ttran"