<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Claude-Code on Lessons learned</title><link>https://eghuro.cz/tags/claude-code/</link><description>Recent content in Claude-Code on Lessons learned</description><generator>Hugo</generator><language>en-us</language><copyright>© Alexandr Mansurov · Licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)</copyright><lastBuildDate>Sat, 31 Oct 2026 22:29:00 +0100</lastBuildDate><atom:link href="https://eghuro.cz/tags/claude-code/index.xml" rel="self" type="application/rss+xml"/><item><title>Backing up ~/.claude and ~/.codex without archiving your AI logins</title><link>https://eghuro.cz/posts/ai-client-local-state-backup/</link><pubDate>Sat, 31 Oct 2026 22:29:00 +0100</pubDate><guid>https://eghuro.cz/posts/ai-client-local-state-backup/</guid><description>&lt;p&gt;If you back up the whole &lt;code&gt;~/.claude&lt;/code&gt; or &lt;code&gt;~/.codex&lt;/code&gt; folder, the restore works, and the backup now holds everything an infostealer wants. The same folder holds your settings, every session transcript in plaintext, and, on Linux, the login itself:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;~/.claude/.credentials.json # Claude Code login, mode 0600&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;~/.codex/auth.json # Codex login when file storage is in use&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;~/.claude/projects/*/*.jsonl # transcripts: prompts, tool output, file contents&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Split the folder into two lists. Back up what you wrote: settings, instructions, skills, hooks, and memory. Keep transcripts as optional encrypted data. Leave login files out of routine backups and log in again after a restore.&lt;/p&gt;&#10;&#10;&lt;h2 id="where-ai-client-state-lives-on-macos-and-linux"&gt;Where AI client state lives on macOS and Linux &lt;/h2&gt;&#10;&lt;p&gt;I checked these paths against Claude Code 2.1.316, Claude Desktop for Linux 4.4212.0, ChatGPT for macOS 1.2026.183, the Codex desktop app for macOS 26.1027.30824, and Codex source at commit &lt;code&gt;67f0994&lt;/code&gt;. &amp;ldquo;Documented&amp;rdquo; means the vendor docs name the path. &amp;ldquo;Observed&amp;rdquo; means I found it by inspecting the official build so that it can change in any release.&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Client&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;State root&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Login material&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Evidence&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Claude Code&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;~/.claude/&lt;/code&gt; and &lt;code&gt;~/.claude.json&lt;/code&gt;, both under &lt;code&gt;CLAUDE_CONFIG_DIR&lt;/code&gt; if set&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Keychain on macOS, or &lt;code&gt;~/.claude/.credentials.json&lt;/code&gt; when the Keychain rejects the write (locked in an SSH session, for example). On Linux, always that file&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Documented&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Claude Desktop&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;macOS: &lt;code&gt;~/Library/Application Support/Claude/&lt;/code&gt;. Linux (beta): &lt;code&gt;${XDG_CONFIG_HOME:-~/.config}/Claude/&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Electron cookies and Local Storage, extension secrets encrypted with &lt;code&gt;safeStorage&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;MCP config documented, rest observed&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;ChatGPT desktop, macOS only&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;~/Library/Application Support/com.openai.chat/&lt;/code&gt;, &lt;code&gt;~/Library/Group Containers/2DC432GLL2.group.com.openai.chat/&lt;/code&gt;, &lt;code&gt;~/Library/Preferences/com.openai.chat.plist&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Keychain items in OpenAI&amp;rsquo;s access group&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Observed&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Codex desktop, macOS only&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;~/Library/Application Support/Codex/&lt;/code&gt;, plus &lt;code&gt;~/.codex&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Shared with Codex CLI&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Observed&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Codex CLI&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;${CODEX_HOME:-~/.codex}&lt;/code&gt;. &lt;code&gt;CODEX_SQLITE_HOME&lt;/code&gt;, &lt;code&gt;sqlite_home&lt;/code&gt; and &lt;code&gt;log_dir&lt;/code&gt; can move parts of it&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;auth.json&lt;/code&gt; in &lt;code&gt;file&lt;/code&gt; mode or as the &lt;code&gt;auto&lt;/code&gt; fallback; &lt;code&gt;.credentials.json&lt;/code&gt; for MCP OAuth without a keyring&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Documented and source&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;Desktop sessions that run Claude Code write to &lt;code&gt;~/.claude/&lt;/code&gt;, not to the Electron folder, so include both.&lt;/p&gt;&#10;&#10;&lt;h2 id="what-to-back-up-and-what-to-leave-out"&gt;What to back up and what to leave out &lt;/h2&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Treatment&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Claude Code&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Codex&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Back up, versioned&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;settings.json&lt;/code&gt;, &lt;code&gt;CLAUDE.md&lt;/code&gt;, &lt;code&gt;rules/&lt;/code&gt;, &lt;code&gt;skills/&lt;/code&gt;, &lt;code&gt;commands/&lt;/code&gt;, &lt;code&gt;agents/&lt;/code&gt;, &lt;code&gt;workflows/&lt;/code&gt;, &lt;code&gt;output-styles/&lt;/code&gt;, &lt;code&gt;keybindings.json&lt;/code&gt;, &lt;code&gt;projects/*/memory/&lt;/code&gt;, &lt;code&gt;agent-memory/&lt;/code&gt;, &lt;code&gt;plugins/installed_plugins.json&lt;/code&gt; as a manifest&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;config.toml&lt;/code&gt;, &lt;code&gt;*.config.toml&lt;/code&gt;, &lt;code&gt;hooks.json&lt;/code&gt;, &lt;code&gt;rules/&lt;/code&gt;, &lt;code&gt;skills/&lt;/code&gt;, agent-role files&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Back up after a secret check&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;~/.claude.json&lt;/code&gt; (personal MCP servers, trust decisions)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;MCP &lt;code&gt;env&lt;/code&gt; blocks in &lt;code&gt;config.toml&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Optional, encrypted, short retention&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;projects/*/*.jsonl&lt;/code&gt;, &lt;code&gt;subagents/&lt;/code&gt;, &lt;code&gt;tool-results/&lt;/code&gt;, &lt;code&gt;history.jsonl&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;sessions/YYYY/MM/DD/rollout-*.jsonl&lt;/code&gt;, &lt;code&gt;archived_sessions/&lt;/code&gt;, &lt;code&gt;history.jsonl&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Exclude&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;.credentials.json&lt;/code&gt;, &lt;code&gt;file-history/&lt;/code&gt;, &lt;code&gt;paste-cache/&lt;/code&gt;, &lt;code&gt;debug/&lt;/code&gt;, &lt;code&gt;shell-snapshots/&lt;/code&gt;, &lt;code&gt;session-env/&lt;/code&gt;, &lt;code&gt;backups/&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;auth.json&lt;/code&gt;, &lt;code&gt;.credentials.json&lt;/code&gt;, &lt;code&gt;log/&lt;/code&gt;, &lt;code&gt;logs_2.sqlite&lt;/code&gt;, &lt;code&gt;shell_snapshots/&lt;/code&gt;, &lt;code&gt;packages/&lt;/code&gt;, caches&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;For Claude Desktop, keep &lt;code&gt;claude_desktop_config.json&lt;/code&gt; after checking its MCP &lt;code&gt;env&lt;/code&gt; blocks, plus a list of installed extensions and where each came from. Reinstall extensions instead of restoring their executable caches. The ChatGPT app&amp;rsquo;s local databases are caches of a server-side account; use the account&amp;rsquo;s data export for conversations you want to keep.&lt;/p&gt;&#10;&#10;&lt;h2 id="why-copying-the-whole-folder-is-the-wrong-default"&gt;Why copying the whole folder is the wrong default &lt;/h2&gt;&#10;&lt;p&gt;Transcripts are plaintext. &lt;a&#10; href="https://code.claude.com/docs/en/claude-directory.md"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;Anthropic&amp;rsquo;s docs&lt;/a&gt;&#10; say that anything passing through a tool is written to the session &lt;code&gt;.jsonl&lt;/code&gt;, so a &lt;code&gt;.env&lt;/code&gt; file a tool reads or a token a command prints is in there. Codex rollouts record tool activity the same way.&lt;/p&gt;&#10;&lt;p&gt;Your backup also keeps transcripts after the client has deleted them. Claude Code deletes transcripts after &lt;code&gt;cleanupPeriodDays&lt;/code&gt; (default 30), but keeps sessions started, or last continued in Claude Desktop or Cowork at any age unless you set &lt;code&gt;desktopSessionCleanupPeriodDays&lt;/code&gt;. A backup with a year of history still holds a transcript eleven months after the client deleted it.&lt;/p&gt;&#10;&lt;p&gt;A restore can also run code. Hooks, MCP server commands, skills, plugins, desktop extensions, and shell snapshots all execute something. If malware edited them, a full restore puts it back on the clean machine.&lt;/p&gt;&#10;&lt;p&gt;The Keychain protects less than it seems to. It stops someone who copies the disk offline, but malware running as your user can still request the item. The Linux desktop build can also fall back to Chromium&amp;rsquo;s basic password store when no wallet is running.&lt;/p&gt;&#10;&#10;&lt;h2 id="dead-ends-worth-skipping"&gt;Dead ends worth skipping &lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Guessing Linux paths from macOS ones.&lt;/strong&gt; Replacing &lt;code&gt;Library/Application Support&lt;/code&gt; with &lt;code&gt;.config&lt;/code&gt; works for Claude Desktop, which has a Linux beta. OpenAI ships no Linux desktop app for ChatGPT or Codex. The Codex source has Linux branches, but there is no supported Linux build. On Linux, your ChatGPT state lives in the browser profile.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Copying only the &lt;code&gt;.sqlite&lt;/code&gt; files.&lt;/strong&gt; Codex keeps state in &lt;code&gt;state_5.sqlite&lt;/code&gt;, &lt;code&gt;thread_history_1.sqlite&lt;/code&gt;, &lt;code&gt;memories_1.sqlite&lt;/code&gt; and &lt;a&#10; href="https://github.com/openai/codex/blob/67f0994b532b8740f14b5b738af7b7b2a915b1d6/codex-rs/state/src/sqlite.rs#L48-L53"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;others&lt;/a&gt;&#10;. A live copy without the &lt;code&gt;-wal&lt;/code&gt; and &lt;code&gt;-shm&lt;/code&gt; sidecars can be incomplete or corrupt. Stop the client and copy all three.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Treating the desktop database as the archive.&lt;/strong&gt; The Codex desktop app can rebuild its index from the rollout JSONL files, but metadata that lives only in the database is lost, back up the JSONL files.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Assuming &lt;code&gt;claude purge&lt;/code&gt; cleaned everything.&lt;/strong&gt; It doesn&amp;rsquo;t touch &lt;code&gt;shell-snapshots/&lt;/code&gt;, &lt;code&gt;backups/&lt;/code&gt; (older copies of &lt;code&gt;~/.claude.json&lt;/code&gt;) or files written by &lt;code&gt;/heapdump&lt;/code&gt;. According to Anthropic, a heap snapshot contains the full conversation and any credentials the process held.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&#10;&lt;h2 id="restore-into-a-staging-profile"&gt;Restore into a staging profile &lt;/h2&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Reinstall the signed apps, plugins, and extensions from their sources.&lt;/li&gt;&#10;&lt;li&gt;Restore your config, then read every hook, MCP command, and executable path before the first launch.&lt;/li&gt;&#10;&lt;li&gt;Restore transcripts or memory only if you need them.&lt;/li&gt;&#10;&lt;li&gt;Log in again. Don&amp;rsquo;t restore &lt;code&gt;auth.json&lt;/code&gt;, &lt;code&gt;.credentials.json&lt;/code&gt;, cookies, or Keychain exports.&lt;/li&gt;&#10;&lt;li&gt;Resume one session and confirm that it uses the right account and repository.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&#10;&lt;h2 id="verify-the-backup-set"&gt;Verify the backup set &lt;/h2&gt;&#10;&lt;p&gt;To list what exists and how large it is, without printing any contents, run this on the machine you back up:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; p in &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CLAUDE_CONFIG_DIR&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="p"&gt;/.claude&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude.json&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CODEX_HOME&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="p"&gt;/.codex&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;XDG_CONFIG_HOME&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="p"&gt;/.config&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/Claude&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/Library/Application Support/Claude&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/Library/Application Support/Codex&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/Library/Application Support/com.openai.chat&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/Library/Group Containers/2DC432GLL2.group.com.openai.chat&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;[&lt;/span&gt; -e &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; du -sh &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;done&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then check that no login file ended up in the backup:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;find /path/to/mounted/backup &lt;span class="se"&gt;\(&lt;/span&gt; -name .credentials.json -o -name auth.json &lt;span class="se"&gt;\)&lt;/span&gt; -print&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It should print nothing. If it prints anything, that backup is a credential store: encrypt it, shorten its retention, or rotate the logins.&lt;/p&gt;&#10;&#10;&lt;h2 id="if-malware-may-already-have-run"&gt;If malware may already have run &lt;/h2&gt;&#10;&lt;p&gt;Don&amp;rsquo;t treat a restore as the cleanup. From a clean device, revoke your Claude and ChatGPT sessions, then rotate API keys, MCP and connector credentials, and any secret that appeared in a transcript or in tool output. Any backup the malware could reach counts as exposed. Rebuild the user profile, restore only config you have reviewed, and log in again.&lt;/p&gt;&#10;&#10;&lt;h2 id="caveats"&gt;Caveats &lt;/h2&gt;&#10;&lt;p&gt;Paths and retention rules change between releases. The ChatGPT and desktop paths come from build inspection, not from a published schema, so check them against your installed versions before you write a backup job. Sources: the &lt;a&#10; href="https://code.claude.com/docs/en/claude-directory.md"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;&lt;code&gt;.claude&lt;/code&gt; directory reference&lt;/a&gt;&#10;, &lt;a&#10; href="https://code.claude.com/docs/en/authentication.md#credential-management"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;Claude Code credential management&lt;/a&gt;&#10;, &lt;a&#10; href="https://learn.chatgpt.com/docs/config-file/config-advanced.md#config-and-state-locations"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;Codex config and state locations&lt;/a&gt;&#10;, &lt;a&#10; href="https://learn.chatgpt.com/docs/auth.md#credential-storage"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;Codex credential storage&lt;/a&gt;&#10;, &lt;a&#10; href="https://modelcontextprotocol.io/quickstart/user"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;Claude Desktop MCP config paths&lt;/a&gt;&#10;, and &lt;a&#10; href="https://www.electronjs.org/docs/latest/api/app#appgetpathname"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;Electron&amp;rsquo;s &lt;code&gt;app.getPath()&lt;/code&gt;&lt;/a&gt;&#10;.&lt;/p&gt;&#10;&lt;p&gt;Back up the config you wrote, and leave out the files the app uses for login. Run the &lt;code&gt;find&lt;/code&gt; check above against your latest backup tonight.&lt;/p&gt;&#10;</description></item><item><title>Claude Code over SSH: submit long-running work to OpenPBS</title><link>https://eghuro.cz/posts/claude-code-ssh-openpbs-setup/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://eghuro.cz/posts/claude-code-ssh-openpbs-setup/</guid><description>&lt;p&gt;Claude Code can prepare a computation on your laptop and operate a remote cluster through ordinary SSH commands. In this setup, Claude Code ran locally on macOS; source uploads, compilation, queue submissions, and checks ran on a Linux server. OpenPBS handled the long-running computation after submission, so the laptop didn&amp;rsquo;t need to keep an SSH session open.&lt;/p&gt;&#10;&lt;p&gt;The operating rule was explicit: work estimated at one hour or more belonged in PBS. That threshold was a local policy, not an OpenPBS default. Use your site&amp;rsquo;s rules, including its restrictions on short tests and compilation on login nodes.&lt;/p&gt;&#10;&#10;&lt;h2 id="establish-the-ssh-connection-yourself"&gt;Establish the SSH connection yourself &lt;/h2&gt;&#10;&lt;p&gt;Configure authentication and verify the server&amp;rsquo;s host key before asking an agent to connect. For example, in your local &lt;code&gt;~/.ssh/config&lt;/code&gt;:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-sshconfig" data-lang="sshconfig"&gt;Host compute-login&#10; HostName 192.168.0.2&#10; User researcher&#10; BatchMode yes&#10; ConnectTimeout 10&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The address and account are placeholders. With your site&amp;rsquo;s actual values and authentication configured, run this from the laptop:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh compute-login &lt;span class="s1"&gt;&amp;#39;hostname; pwd; command -v qsub; command -v python3&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;BatchMode&lt;/code&gt; makes missing authentication fail instead of waiting for a password. It does not approve the connection or replace host-key verification. Give Claude permission for the intended remote actions through your normal tool-approval workflow; never paste a private key into the conversation.&lt;/p&gt;&#10;&#10;&lt;h2 id="make-the-execution-boundary-explicit"&gt;Make the execution boundary explicit &lt;/h2&gt;&#10;&lt;p&gt;A useful project instruction is:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Claude runs on the laptop. Use SSH for remote operations.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Keep this project&amp;#39;s remote files in ~/batch-demo.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Submit tasks estimated at one hour or more through OpenPBS.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Follow site policy for shorter tests; do not bypass the scheduler.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Record the job ID after submission. Check logs and expected outputs before reporting completion. Ask before cancellation or deletion.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Save this with the project&amp;rsquo;s agent instructions. A prose rule still needs checking: our early sessions launched substantial work directly before the PBS rule was captured. Also, having a GPU on a login host does not grant permission to use it.&lt;/p&gt;&#10;&#10;&lt;h2 id="submit-a-small-smoke-test-first"&gt;Submit a small smoke test first &lt;/h2&gt;&#10;&lt;p&gt;These are generalized setup examples, not a copy of the original scientific workload. Create &lt;code&gt;run.py&lt;/code&gt; locally:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;socket&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;sys&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;worker host: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gethostname&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;python: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;smoke test complete&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Create &lt;code&gt;run.pbs&lt;/code&gt; beside it:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/bin/bash&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#PBS -N agent-smoke&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#PBS -l select=1:ncpus=1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#PBS -l walltime=00:05:00&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#PBS -j oe&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -euo pipefail&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PBS_O_WORKDIR&lt;/span&gt;&lt;span class="p"&gt;:?PBS submission directory missing&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;/.venv/bin/python&amp;#34;&lt;/span&gt; -u run.py&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;From the laptop, prepare and submit it:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh compute-login &lt;span class="s1"&gt;&amp;#39;mkdir -p ~/batch-demo &amp;amp;&amp;amp; cd ~/batch-demo &amp;amp;&amp;amp; python3 -m venv .venv&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;scp run.py run.pbs compute-login:batch-demo/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh compute-login &lt;span class="s1"&gt;&amp;#39;cd ~/batch-demo &amp;amp;&amp;amp; qsub run.pbs&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Record the exact job ID that &lt;code&gt;qsub&lt;/code&gt; returns. The batch script selects its directory and interpreter without depending on an activated interactive shell. Install real dependencies according to site policy and check that the environment and project directory are accessible on the execution node. Shared paths and compatible Python installations are prerequisites, not promises made by SSH.&lt;/p&gt;&#10;&lt;p&gt;For GPU work, request the site&amp;rsquo;s documented GPU resources. The original array used &lt;code&gt;select=1:ncpus=2:ngpus=1&lt;/code&gt; per element. GPU visibility variables and binding depend on cluster configuration; inspect them inside an allocation rather than choosing an arbitrary device from the login host.&lt;/p&gt;&#10;&#10;&lt;h2 id="follow-the-job-through-its-outputs"&gt;Follow the job through its outputs &lt;/h2&gt;&#10;&lt;p&gt;On the login host, use &lt;code&gt;qstat -f&lt;/code&gt; with the recorded ID for detailed status. Read the output file named in that status. Sites retaining finished-job history may also support &lt;code&gt;qstat -xf&lt;/code&gt; for accounting. The &lt;a&#10; href="https://github.com/openpbs/openpbs/blob/master/doc/man1/qsub.1B"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;upstream qsub manual&lt;/a&gt;&#10; documents submission options and &lt;code&gt;PBS_O_WORKDIR&lt;/code&gt;; the &lt;a&#10; href="https://github.com/openpbs/openpbs/blob/master/doc/man1/qstat.1B"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;qstat manual&lt;/a&gt;&#10; covers status queries.&lt;/p&gt;&#10;&lt;p&gt;An empty queue is insufficient evidence of success. For this smoke test, check the reported worker and Python path and the completion line. For production, also verify expected task outcomes and output contents.&lt;/p&gt;&#10;&lt;p&gt;The subsequent failures were about execution ownership and verification:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a&#10; href="https://eghuro.cz/posts/remote-failed-build-stale-executable"&#10; &#10; data-rel="prefetch"&#10; &#10; &#10;&gt;A failed remote build launched an old executable&lt;/a&gt;&#10;.&lt;/li&gt;&#10;&lt;li&gt;&lt;a&#10; href="https://eghuro.cz/posts/resumable-worker-concurrent-launch"&#10; &#10; data-rel="prefetch"&#10; &#10; &#10;&gt;A second detached launch collided with resumable work&lt;/a&gt;&#10;.&lt;/li&gt;&#10;&lt;li&gt;&lt;a&#10; href="https://eghuro.cz/posts/openpbs-resume-stable-shard-ids"&#10; &#10; data-rel="prefetch"&#10; &#10; &#10;&gt;PBS array resume needed stable task IDs&lt;/a&gt;&#10;.&lt;/li&gt;&#10;&lt;li&gt;&lt;a&#10; href="https://eghuro.cz/posts/rsync-atomic-archives-incomplete-release"&#10; &#10; data-rel="prefetch"&#10; &#10; &#10;&gt;Archive transfer started before the release was complete&lt;/a&gt;&#10;.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Before handing over a long run, submit the smoke test and check its output on the execution node&amp;rsquo;s actual environment.&lt;/p&gt;&#10;</description></item></channel></rss>