> ## Documentation Index
> Fetch the complete documentation index at: https://docs.artifacta.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Store session transcripts

> Store, filter, and automatically capture session transcripts with Artifacta.

Artifacta stores transcripts as ordinary, opaque artifacts. Transcript sugar fills the
conventional `metadata.type="transcript"` tag and the
`application/x-ndjson` content type without adding a transcript resource, endpoint, or
server-side schema. Start with the checked-in
[recommended NDJSON example](https://github.com/SagaPeak/artifacta/blob/main/docs/examples/transcript_example.ndjson),
or store your agent's existing transcript format.

## Use one session for the transcript and its outputs

Give the transcript and every artifact produced during a run the same session ID. You can
then list the whole run or filter it to the transcript alone.

```bash theme={null}
artifacta push report.pdf --session run_42
artifacta push transcript.ndjson --session run_42 --transcript

# Everything produced in the session
artifacta ls --session run_42

# Only artifacts tagged as transcripts
artifacta ls --session run_42 --transcript
```

`--transcript` defaults the upload to `content_type=application/x-ndjson` and adds
`metadata.type=transcript`. It does not change output, errors, TTL, sealing, deduplication,
idempotency, or upload limits.

## Explicit values take precedence

Write precedence is independent for the two defaults:

1. An explicit content type always wins; otherwise a true transcript flag supplies
   `application/x-ndjson` before filename MIME guessing.
2. The presence of an explicit metadata `type` key always wins, including `type=""`;
   otherwise a true transcript flag supplies `type="transcript"`.
3. When the flag is omitted or false, existing MIME guessing and metadata behavior remain
   unchanged.

On list calls, an explicit metadata `type` filter wins, including an empty value. Otherwise
a true transcript flag adds exactly one `metadata.type=transcript` filter. Other metadata is
preserved in every case.

```bash theme={null}
# Explicit content type wins independently.
artifacta push transcript.jsonl --session run_42 --transcript \
  --content-type application/jsonl

# Explicit metadata type wins independently.
artifacta push transcript.ndjson --session run_42 --transcript \
  --meta type=custom_transcript

# The explicit list filter wins; this filters custom_transcript artifacts.
artifacta ls --session run_42 --transcript --meta type=custom_transcript
```

## Python SDK

`transcript` is a keyword-only boolean on the existing `push()` and `list()` methods. Their
public signatures are:

```python theme={null}
def push(
    self,
    path: str | Path | None = None,
    *,
    content: bytes | None = None,
    filename: str | None = None,
    content_type: str | None = None,
    session_id: str | None = None,
    agent_id: str | None = None,
    metadata: dict[str, str] | None = None,
    ttl: str | None = None,
    idempotency_key: str | None = None,
    presigned: bool = False,
    transcript: bool = False,
) -> Artifact: ...
```

```python theme={null}
def list(
    self,
    *,
    session_id: str | None = None,
    agent_id: str | None = None,
    metadata: dict[str, str] | None = None,
    limit: int = 50,
    auto_paginate: bool = True,
    cursor: str | None = None,
    transcript: bool = False,
) -> ListResult: ...
```

Use either a path or the existing `content` plus `filename` form:

```python theme={null}
from artifacta import Client

client = Client()
artifact = client.push(
    path="transcript.ndjson",
    session_id="run_42",
    transcript=True,
)
transcripts = client.list(session_id="run_42", transcript=True)
```

## MCP tools

Both the TypeScript and Python MCP servers expose the same optional boolean on the existing
tools. `store_artifact` remains a `writeIdempotent` tool, and `list_artifacts` remains a
`safe` tool.

```json store_artifact arguments theme={null}
{
  "filename": "transcript.ndjson",
  "path": "/workspace/transcript.ndjson",
  "session_id": "run_42",
  "transcript": true
}
```

```json list_artifacts arguments theme={null}
{
  "session_id": "run_42",
  "transcript": true
}
```

The write flag works with both MCP content and path uploads. The same explicit-value
precedence above applies to `content_type` and `metadata.type`; list calls preserve an
explicit `metadata.type` filter.

## Call the REST API directly

The REST API is unchanged. API-direct callers express the convention with existing
`metadata`, `content_type`, and metadata-filter parameters. The API does not infer NDJSON
from `metadata.type` alone, so send the desired content type explicitly.

### Multipart upload

Multipart metadata is a JSON string. `file` and `content_type` are separate existing form
fields.

```bash theme={null}
curl -X POST https://api.artifacta.io/v1/artifacts \
  -H "Authorization: Bearer $ARTIFACTA_API_KEY" \
  -F 'file=@transcript.ndjson' \
  -F 'session_id=run_42' \
  -F 'metadata={"type":"transcript"}' \
  -F 'content_type=application/x-ndjson'
```

### JSON-body upload

JSON uploads carry base64 content, `content_encoding="base64"`, a filename, object
metadata, and an explicit content type.

```bash theme={null}
CONTENT_B64="$(base64 < transcript.ndjson | tr -d '\n')"
jq -n --arg content "$CONTENT_B64" '{
  content: $content,
  content_encoding: "base64",
  filename: "transcript.ndjson",
  session_id: "run_42",
  metadata: {type: "transcript"},
  content_type: "application/x-ndjson"
}' | curl -X POST https://api.artifacta.io/v1/artifacts \
  -H "Authorization: Bearer $ARTIFACTA_API_KEY" \
  -H 'Content-Type: application/json' \
  --data-binary @-
```

### List transcripts

Use the existing metadata query and URL-encode the query string:

```bash theme={null}
curl --get https://api.artifacta.io/v1/artifacts \
  -H "Authorization: Bearer $ARTIFACTA_API_KEY" \
  --data-urlencode 'session_id=run_42' \
  --data-urlencode 'metadata.type=transcript'
```

There is no REST transcript field or query parameter on create or list requests.

## Capture Claude Code sessions automatically

This recipe uses Claude Code's client-side `SessionEnd` hook to invoke the same Artifacta
CLI push shown above. Artifacta does not run a listener or provide a separate capture
service.

<Tip>
  **In Claude Code with the Artifacta plugin:** install with
  `/plugin marketplace add SagaPeak/artifacta-mcp` then
  `/plugin install artifacta@artifacta`, and ask in natural language — for example,
  *"use artifacta to upload this session's transcript"*. The plugin's
  `capture-transcript` skill locates and verifies the live session transcript,
  pushes a snapshot, and can offer to set up this hook for you.
</Tip>

### Prerequisites

* Install and authenticate the `artifacta` CLI, and ensure `artifacta` is on `PATH`.
* Install `jq` and ensure it is on `PATH`.
* The documented hook expects JSON on stdin with `session_id`, `transcript_path`, and
  `reason`. It reads the first two fields; `transcript_path` must name an existing file.

Add this hook to `~/.claude/settings.json`:

```json theme={null}
{
  "hooks": {
    "SessionEnd": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/push-transcript.sh"
          }
        ]
      }
    ]
  }
}
```

Create `~/.claude/hooks/push-transcript.sh` with these exact contents:

```bash theme={null}
#!/usr/bin/env bash
set -euo pipefail

INPUT="$(cat)"
SESSION_ID="$(echo "$INPUT" | jq -r '.session_id')"
TRANSCRIPT_PATH="$(echo "$INPUT" | jq -r '.transcript_path')"

if [[ -z "$SESSION_ID" || "$SESSION_ID" == "null" ]]; then
  echo "artifacta SessionEnd hook: missing session_id" >&2
  exit 1
fi
if [[ -z "$TRANSCRIPT_PATH" || "$TRANSCRIPT_PATH" == "null" || ! -f "$TRANSCRIPT_PATH" ]]; then
  echo "artifacta SessionEnd hook: missing transcript file: ${TRANSCRIPT_PATH:-}" >&2
  exit 1
fi

# Model capture (best-effort): metadata.model is the LAST MAIN-LOOP assistant
# model — sidechain (subagent) entries and "<synthetic>" placeholders are
# excluded. models_used lists every distinct model observed, sidechains
# included. Extraction failure degrades to a plain push, never a lost one.
MODEL="$(jq -r 'select(.type == "assistant" and .isSidechain != true) | .message.model // empty | select(. != "<synthetic>")' "$TRANSCRIPT_PATH" 2>/dev/null | tail -n 1 || true)"
MODELS_USED="$(jq -r 'select(.type == "assistant") | .message.model // empty | select(. != "<synthetic>")' "$TRANSCRIPT_PATH" 2>/dev/null | sort -u | paste -sd, - || true)"

if [[ -n "$MODEL" ]]; then
  artifacta push "$TRANSCRIPT_PATH" --session "$SESSION_ID" --transcript --meta "model=$MODEL" --meta "model_source=transcript" --meta "models_used=$MODELS_USED"
else
  artifacta push "$TRANSCRIPT_PATH" --session "$SESSION_ID" --transcript
fi
```

Make the script executable:

```bash theme={null}
mkdir -p ~/.claude/hooks
chmod 700 ~/.claude/hooks/push-transcript.sh
```

Malformed JSON causes `jq` to fail. Missing, null, or empty required fields and a nonexistent
transcript file also fail validation. Any non-zero Artifacta CLI exit propagates as a
non-zero Claude Code hook failure. Input validation and the Artifacta push still fail hard:
there are no retries and no swallowed push errors. Model extraction alone is best-effort; if
it fails or finds no main-loop model, the script pushes the transcript without model metadata
rather than losing the upload.

<Warning>
  `SessionEnd`, its lifecycle, and the `session_id`, `transcript_path`, and `reason` stdin
  fields are an external contract owned by Anthropic. Artifacta does not pin or abstract
  that contract. Re-check Anthropic's hook documentation after upgrading Claude Code.
</Warning>

## Model capture

The hook script above also records which model produced the session, straight
from the transcript itself:

* `metadata.model` — the last **main-loop** assistant model in the transcript.
  Sidechain (subagent) entries and synthetic placeholders are excluded via the
  transcript's `isSidechain` marker, so a session that dispatched subagents on
  a different model still attributes to the orchestrating model — the model
  that directed the work and decided what got stored.
* `metadata.model_source=transcript` — marks the value as machine-captured.
  Absence of `model_source` means the model was declared by the caller.
* `metadata.models_used` — every distinct model observed in the transcript,
  comma-separated (e.g. `claude-fable-5,claude-sonnet-5`), including subagent
  sidechains.

Artifacta records the model automatically from the agent runtime's own session
log and freezes it at store time — a captured producer claim, not a cryptographic attestation.
Two honest limits: session-level capture
approximates per-artifact authorship — the transcript proves which models participated in the session, not which one emitted a specific artifact's
bytes; and subagents spawned as separate sessions don't appear in the main
transcript, so `models_used` is "models observed", never exhaustive.

For pushes outside the hook, declare the model yourself: `artifacta push
report.pdf --model claude-fable-5` (shorthand for `--meta model=`), or export
`ARTIFACTA_MODEL` once in a wrapper script — explicit flags always beat the
environment. If a wholly-delegated subagent authored the artifact, pass that
subagent's model explicitly.

Published pages render `metadata.model` in the page receipt automatically.

## Verify a captured transcript

```bash theme={null}
artifacta ls --session run_42 --transcript
artifacta pull <artifact_id> -o ./captured-transcript.ndjson
cmp transcript.ndjson ./captured-transcript.ndjson
```

An unfiltered session list should include both the transcript and the artifacts produced by
the run. The transcript-filtered list should include only artifacts whose explicit metadata
type is `transcript`.

For a later native Claude Code lifecycle check, use the repository's
[reproducible `SessionEnd` operator validation](https://github.com/SagaPeak/artifacta/blob/main/docs/qa/mcp/transcript-v1-claude-code-validation.md).
That optional follow-up is not a PR blocker: automated exact-hook tests and live execution
with canonical synthetic stdin already passed.

## Security: audit before you push

Artifacta does **not** automatically scan, redact, or block secrets in transcript uploads.
Callers are responsible for reviewing and redacting transcript content before storage.

For an optional source-checkout audit, run the repository's existing scanner against a
directory before pushing it:

```bash theme={null}
cd /path/to/artifacta/mcp/typescript
npx tsx scripts/secret-audit.ts /path/to/transcript-directory
```

The scanner exits `0` when clean, `1` when it finds credential-shaped content, and `2` for
a usage error. It reports locations and pattern names without echoing matched values. The
repeatable `--allow <string>` option can suppress a known benign match. This user-run audit
is optional, is available only from a source checkout, and is not an Artifacta push gate;
you may wire it into your own pre-push hook if desired.

## Codex plugin

Artifacta plugin `1.1.0` supports verified transcript capture in Codex. Install and
authenticate it with the [Codex plugin guide](/mcp/install/codex-plugin), then start a new
thread so Codex loads the plugin's skills and hooks.

For an immediate snapshot, ask:

> "Use Artifacta to capture this Codex session's transcript."

The `capture-transcript` skill searches Codex's rollout files for a distinctive phrase from
the current conversation and refuses to continue unless it identifies exactly one regular
file. It copies that live rollout to a private snapshot, uploads the snapshot through
Artifacta MCP `store_artifact`, and removes the private copy after a successful upload.
Codex transcript capture never invokes the local Artifacta CLI or sends a local filesystem
path to the hosted MCP server.

For one capture at the current thread's next `Stop`, include the explicit flag:

> "Use Artifacta to capture this Codex session's transcript --automatic."

`--automatic` arms one unredacted snapshot for this thread only. It does not enable
every-turn capture, future-thread capture, or a background uploader. Open `/hooks`, review
the bundled Artifacta hook, and trust its current definition; Codex skips untrusted
non-managed hooks.

An immediate mid-turn snapshot may not include the request that triggered capture or the
assistant response reporting its result. Retrieve captured records with `list_artifacts`
using the returned session ID and `transcript=true`.

<Warning>
  Codex snapshots are uploaded without redaction and can include prompts, tool arguments,
  tool results, credentials, and other sensitive text. Review the session before requesting
  capture.
</Warning>

## Limitations and hard exclusions

* Transcript payloads remain opaque bytes. NDJSON is recommended, not a mandatory schema.
* There is no incremental, streaming, chunked, or per-turn upload/reassembly behavior.
* There are no dedicated `push_transcript`, `get_transcript`, `pull-transcript`, or MCP
  `get_transcript` APIs or tools.
* There is no server-side agent-session detection or continuous capture mechanism. The
  Claude Code and Codex plugin skills perform verified client-side capture.
* There is no server-side enforcement of the `metadata.type` convention and no transcript
  web viewer.
* Existing artifact limits, retention/TTL choices, session seals, and errors apply unchanged.
