Improvised log export from Claude Code on the web

Claude Code on the web is a research feature on claude.ai that lets you run Claude Code in a container on servers operated by Anthropic. Some limitations apply. For example, there is no /export command, and you cannot shell in. If you resume a session locally with --teleport, you only get the chat log (transcript) since the last compaction. Your conversations are not included in a data takeout. This means exporting an entire conversation when you want to isn’t trivial.

Below is a prompt designed to work around those limitations. I have used a previous, GitHub-only version of the prompt in many sessions. With the addition of the file-send feature to Claude Code on the web (tested in September 2026), Claude can send you small archives directly. The measured file-size limit is 30 MiB. Larger files are refused with a message like 32.0 MiB exceeds the 30 MiB upload limit to the model.

Two versions: one for file-send and one for GitHub.

Please export this conversation so I can keep it locally.

1. COPY FIRST. The transcripts live under ~/.claude/projects/ in a
   directory named for the working directory with slashes turned into
   dashes. Copy that whole project directory — not one session file; it
   holds several, plus a <uuid>/ subdirectory of sidecar files.

       du -sh ~/.claude/projects/<that-directory>; df -h /tmp
       mkdir -m 700 -p /tmp/conversation-export
       cp -a ~/.claude/projects/<that-directory>/. /tmp/conversation-export/

   Everything below happens on the copy. Do not edit, move, or delete
   anything under ~/.claude/projects/ — that is the live transcript of
   the session you are in, and it is the only copy I have.

   Three things to know rather than discover. The copy is a snapshot and
   will not contain anything after this point, including the export
   itself; that missing tail is the least interesting part of the
   record. The live file is being appended to while cp runs, so the last
   line of the current session's .jsonl may be truncated mid-JSON —
   archiving does not care, a later parser should expect it. And the
   transcript includes your thinking blocks: that is what I am
   archiving, and redaction below covers them too.

2. SCAN THE COPY for credentials: API keys, bearer tokens, passwords.
   The files are far too large to read, so grep for patterns. Use
   gitleaks with --redact if it is installable; do not use a scanner
   that prints raw secrets by default.

   Report each finding WITHOUT PRINTING IT. grep prints what it matches,
   so pipe it — this gives prefix, length, and count and no literal:

       grep -rhoaE '<candidate pattern>' /tmp/conversation-export \
           | awk '{ print substr($0,1,10) "…", length($0) }' \
           | sort | uniq -c

   The first ten characters run a little past the prefix on purpose, so
   two keys of the same shape show up as separate rows. For locations,
   `grep -rlaE '<pattern>' /tmp/conversation-export` lists files without
   printing matches; if you can also name the uuid of each containing
   message, do — JSONL lines are whole messages, so a line number
   locates nothing — but only by a method that prints no match.

   Then STOP and ask me what to do with each one. I decide, because I
   know which of these keys are live, which are already dead, and which
   I want kept in the record. You cannot know that from the file.

   Treat the scan as best-effort: a regex will not find an unstructured
   secret.

3. REDACT THE COPY ONLY, BY SHAPE. Build each pattern from the shape you
   reported, never from the literal — that way the secret appears
   nowhere after step 1: not in a command, not in a file you write, not
   in my reply. For a 108-character key beginning sk-ant-:

       s/sk-ant-[A-Za-z0-9_-]\{101\}/REDACTED-KEY-1/g

   Before applying anything, count what the pattern matches with the
   SAME command used in steps 2 and 5, so the three numbers are
   comparable — grep -c counts lines, not occurrences, and a key twice
   in one message would make a correct pattern look wrong:

       grep -rhoaE '<pattern>' /tmp/conversation-export | wc -l

   If that differs from the count you reported in step 2, the pattern is
   too loose or too tight: fix it and tell me, rather than applying it.
   Two secrets of identical shape will collapse to one placeholder; say
   so if that happens.

   Placeholders must NOT contain the prefix, or step 5 can never reach
   zero: REDACTED-KEY-1, not sk-ant-REDACTED. Then:

       find /tmp/conversation-export -type f \
           -exec env LC_ALL=C grep -IlZ '' {} + \
           | xargs -0r sed -i -f /tmp/redactions.sed

   grep -I keeps sed off the non-text files in tool-results/; LC_ALL=C
   is portability insurance, so only NUL bytes decide what counts as
   binary; -Z with -0 survives spaces in paths.

4. ARCHIVE. Install zstd if it is missing, then:

       tar -C /tmp -I 'zstd -7 --long -T0' -cf \
           /tmp/conversation-export.tar.zst conversation-export

   Use those flags, not `tar -caf`. The -a form runs zstd at level 3
   with a 2 MiB window; -7 --long gives it 128 MiB, which is what lets
   it see the system prompts and file contents repeated across session
   files. Measured on a single 14 MB transcript directory that is 13%
   smaller, and the gap widens with more sessions. It costs the reader
   nothing: plain `tar -xf` extracts it with no extra flags.

5. VERIFY ON THE ARCHIVE, by streaming — do not extract it. Extraction
   would put a third full copy on disk alongside the staging directory
   and the archive.

   Check the archive is whole before you trust any count:

       zstd -t /tmp/conversation-export.tar.zst

   This is not ceremony. A truncated archive makes the search below
   print 0 and exit 0, because grep only sees what tar managed to
   deliver — a clean pass on an archive missing half its data. If
   zstd -t reports an error, stop and tell me; the archive is bad and
   the count means nothing.

   Then, for each secret I told you to replace:

       set -o pipefail
       tar -xOf /tmp/conversation-export.tar.zst \
           | grep -hoaE '<pattern>' | wc -l

   Those counts must be zero. Read the exit status as well as the
   number: with pipefail a broken pipeline still prints 0.

   If any count is NOT zero, stop and tell me which pattern. Do not
   re-run sed on the archive, and do not extract it to fix it in place —
   we go back to step 3 on the staging copy and rebuild.

   If a pattern for something I told you to KEEP still matches, that is
   correct and expected. Report anything else that looks like a
   credential by shape and let me decide; do not treat a nonzero count
   as a problem to solve on your own.

6. SEND me /tmp/conversation-export.tar.zst. A refused send costs
   nothing, so try the whole file first. If it is refused as too large:

       split -b 28M /tmp/conversation-export.tar.zst \
             /tmp/conversation-export.tar.zst.part- \
           && rm /tmp/conversation-export.tar.zst

   The rm matters: without it the next glob picks up the oversized
   original alongside its parts. Send the parts and tell me:

       cat conversation-export.tar.zst.part-* > conversation-export.tar.zst
       tar -xf conversation-export.tar.zst

7. Delete /tmp/conversation-export/, /tmp/redactions.sed, and the
   archive and its parts.

Keep in mind that if your repository is public on GitHub, so will be the export branch.

Please export this conversation so I can keep it locally.

1. COPY FIRST. The transcripts live under ~/.claude/projects/ in a
   directory named for the working directory with slashes turned into
   dashes. Copy that whole project directory — not one session file; it
   holds several, plus a <uuid>/ subdirectory of sidecar files.

       du -sh ~/.claude/projects/<that-directory>; df -h /tmp
       mkdir -m 700 -p /tmp/conversation-export
       cp -a ~/.claude/projects/<that-directory>/. /tmp/conversation-export/

   Everything below happens on the copy. Do not edit, move, or delete
   anything under ~/.claude/projects/ — that is the live transcript of
   the session you are in, and it is the only copy I have.

   Three things to know rather than discover. The copy is a snapshot and
   will not contain anything after this point, including the export
   itself; that missing tail is the least interesting part of the
   record. The live file is being appended to while cp runs, so the last
   line of the current session's .jsonl may be truncated mid-JSON —
   archiving does not care, a later parser should expect it. And the
   transcript includes your thinking blocks: that is what I am
   archiving, and redaction below covers them too.

2. SCAN THE COPY for credentials: API keys, bearer tokens, passwords.
   The files are far too large to read, so grep for patterns. Use
   gitleaks with --redact if it is installable; do not use a scanner
   that prints raw secrets by default.

   Report each finding WITHOUT PRINTING IT. grep prints what it matches,
   so pipe it — this gives prefix, length, and count and no literal:

       grep -rhoaE '<candidate pattern>' /tmp/conversation-export \
           | awk '{ print substr($0,1,10) "…", length($0) }' \
           | sort | uniq -c

   The first ten characters run a little past the prefix on purpose, so
   two keys of the same shape show up as separate rows. For locations,
   `grep -rlaE '<pattern>' /tmp/conversation-export` lists files without
   printing matches; if you can also name the uuid of each containing
   message, do — JSONL lines are whole messages, so a line number
   locates nothing — but only by a method that prints no match.

   Then STOP and ask me what to do with each one. I decide, because I
   know which of these keys are live, which are already dead, and which
   I want kept in the record. You cannot know that from the file.

   Treat the scan as best-effort: a regex will not find an unstructured
   secret.

3. REDACT THE COPY ONLY, BY SHAPE. Build each pattern from the shape you
   reported, never from the literal — that way the secret appears
   nowhere after step 1: not in a command, not in a file you write, not
   in my reply. For a 108-character key beginning sk-ant-:

       s/sk-ant-[A-Za-z0-9_-]\{101\}/REDACTED-KEY-1/g

   Before applying anything, count what the pattern matches with the
   SAME command used in steps 2 and 5, so the three numbers are
   comparable — grep -c counts lines, not occurrences, and a key twice
   in one message would make a correct pattern look wrong:

       grep -rhoaE '<pattern>' /tmp/conversation-export | wc -l

   If that differs from the count you reported in step 2, the pattern is
   too loose or too tight: fix it and tell me, rather than applying it.
   Two secrets of identical shape will collapse to one placeholder; say
   so if that happens.

   Placeholders must NOT contain the prefix, or step 5 can never reach
   zero: REDACTED-KEY-1, not sk-ant-REDACTED. Then:

       find /tmp/conversation-export -type f \
           -exec env LC_ALL=C grep -IlZ '' {} + \
           | xargs -0r sed -i -f /tmp/redactions.sed

   grep -I keeps sed off the non-text files in tool-results/; LC_ALL=C
   is portability insurance, so only NUL bytes decide what counts as
   binary; -Z with -0 survives spaces in paths.

4. ARCHIVE. Install zstd if it is missing, then:

       tar -C /tmp -I 'zstd -7 --long -T0' -cf \
           /tmp/conversation-export.tar.zst conversation-export

   Use those flags, not `tar -caf`. The -a form runs zstd at level 3
   with a 2 MiB window; -7 --long gives it 128 MiB, which is what lets
   it see the system prompts and file contents repeated across session
   files. Measured on a single 14 MB transcript directory that is 13%
   smaller, and the gap widens with more sessions. It costs the reader
   nothing: plain `tar -xf` extracts it with no extra flags.

5. VERIFY ON THE ARCHIVE, by streaming — do not extract it. Extraction
   would put a third full copy on disk alongside the staging directory
   and the archive.

   Check the archive is whole before you trust any count:

       zstd -t /tmp/conversation-export.tar.zst

   This is not ceremony. A truncated archive makes the search below
   print 0 and exit 0, because grep only sees what tar managed to
   deliver — a clean pass on an archive missing half its data. If
   zstd -t reports an error, stop and tell me; the archive is bad and
   the count means nothing.

   Then, for each secret I told you to replace:

       set -o pipefail
       tar -xOf /tmp/conversation-export.tar.zst \
           | grep -hoaE '<pattern>' | wc -l

   Those counts must be zero. Read the exit status as well as the
   number: with pipefail a broken pipeline still prints 0.

   If any count is NOT zero, stop and tell me which pattern. Do not
   re-run sed on the archive, and do not extract it to fix it in place —
   we go back to step 3 on the staging copy and rebuild.

   If a pattern for something I told you to KEEP still matches, that is
   correct and expected. Report anything else that looks like a
   credential by shape and let me decide; do not treat a nonzero count
   as a problem to solve on your own.

6. SPLIT IF NEEDED. GitHub rejects files over 100 MiB and warns at
   50 MiB, so if the archive exceeds ~90 MB:

       split -b 90M /tmp/conversation-export.tar.zst \
             /tmp/conversation-export.tar.zst.part- \
           && rm /tmp/conversation-export.tar.zst

   Without the rm, step 7 stages the oversized original alongside its
   parts and the push is rejected — the exact failure the split exists
   to prevent.

7. COMMIT FROM A SEPARATE WORKTREE. Do not switch branches in the
   session's working tree: `git rm -rf .` there would discard any
   uncommitted change and leave the session on an empty orphan branch
   with nothing to work in.

       git worktree remove --force /tmp/export-wt 2>/dev/null
       git worktree prune; git branch -D export-tmp 2>/dev/null
       git worktree add --detach /tmp/export-wt
       git -C /tmp/export-wt checkout --orphan export-tmp
       git -C /tmp/export-wt rm -rfq .
       cp /tmp/conversation-export.tar.zst* /tmp/export-wt/
       git -C /tmp/export-wt add .
       git -C /tmp/export-wt status --short

   The first two lines make a retry work after an interrupted run. The
   local branch is a throwaway because the remote name is given
   explicitly on the push below; naming it after the remote branch would
   fail on the second export with "a branch named … already exists".
   `git add .` rather than a glob: git -C changes git's directory, not
   the shell's, so a pattern would be expanded against the session's cwd
   — one stale archive there and the add fails with a pathspec error
   about a file that has nothing to do with this export. `git add` also
   refuses paths outside the work tree, which is why the archive is
   copied in first.

   Confirm the staging list is exactly the archive, or exactly its
   parts. If anything else is listed, stop and tell me.

       git -C /tmp/export-wt commit -m "Conversation export"
       git -C /tmp/export-wt push --force origin \
           HEAD:refs/heads/claude/conversation-export
       git worktree remove --force /tmp/export-wt
       git branch -D export-tmp

8. Give me the URL, and tell me these three things, because they are not
   yours to do:

   - I have to delete the branch myself. In testing, this environment's
     push proxy accepted branch creation and force-updates but returned
     HTTP 403 on deletion; if a deletion ever fails that way, leave it
     to me rather than routing around it. The branch name is fixed
     rather than dated for exactly this reason — the next export
     overwrites it instead of stacking up branches neither of us can
     remove. (The cost: two sessions exporting the same project at once
     would overwrite each other.)
   - Anything we did not redact is exposed as of the push, and deleting
     the branch does not undo that: the commit stays fetchable by SHA,
     and in forks and caches, until GitHub garbage-collects it. Purging
     needs GitHub support. If a live credential went up, rotating it is
     the fix; deleting the branch is not.
   - Confirm the session's own tree is untouched:
     `git status --short --branch` should show my original branch and no
     changes. The worktree flow never leaves it, so this is a check, not
     a step to undo.

9. Delete /tmp/conversation-export/, /tmp/redactions.sed, and the
   archive and its parts.

Here is how the prompt was developed. I ran claude --teleport to see if it would download the remote session log. When I looked into ~/.claude/, I saw it had a session log file in JSONL, but it only went as far back as the last compaction. This was when I realized I could ask Claude what was in ~/.claude/ inside its container; then Claude could upload or commit it. Claude Opus 4.7, which I’d worked with in a long session I wanted to preserve, said that of course it could do it and suggested storing the JSONL file on a fresh orphan branch. I found that the session was incomplete. When Opus investigated, it saw the directory was full of session files. They totalled 1 GiB uncompressed, 381 MiB with gzip, and 328 MiB with bzip2 compression. (We’d developed a multimedia project.) GitHub rejected files larger than 100 MiB. Opus worked around this by splitting the tar file with split(1). It worked, and I got the data out.

I asked Opus to turn what we’d done into a reusable prompt, which became the first version of the prompt above. I benchmarked different compressors on the decompressed and joined archive and found that with zstd -7 --long (and higher), you didn’t need to split even a large session (the 1 GiB compressed down to 17 MiB), so I edited the prompt to suggest it. The large compression window was key.

Claude Opus 5 measured the exact size limit for file delivery (30 MiB + 1 byte = 31 457 281 bytes refused, 31 457 280 bytes sent) and proposed adding a rule for redacting secrets. The current version of the prompts is written by Opus 5 with feedback from Fable 5.1.