Visitar URL original
feat: add persist_session option by model-bender · Pull Request #1476 · anthropics/claude-agent-sdk-python · GitHub
Skip to content

feat: add persist_session option - #1476

Open
model-bender wants to merge 1 commit into
anthropics:mainfrom
model-bender:feat/persist-session
Open

model-bender wants to merge 1 commit into
anthropics:mainfrom
model-bender:feat/persist-session

Conversation

@model-bender

Copy link
Copy Markdown

Re-opened from a new fork with the same commits as #1322, which closed when its fork was deleted. Earlier review is on #1322.

Summary

Adds ClaudeAgentOptions.persist_session, the Python counterpart of the TypeScript SDK's persistSession. With persist_session=False the CLI runs with --no-session-persistence and writes no transcript under ~/.claude/projects/, so ephemeral or automated runs leave no session history behind. The default (True) adds nothing to the command line, so existing behaviour is unchanged.

Until now the only way to get this from Python was extra_args={"no-session-persistence": None}.

It follows the TypeScript SDK (checked against @anthropic-ai/claude-agent-sdk 0.3.283):

  • The flag is sent only for an explicit persist_session=False (TS: if (this.options.persistSession === false) args.push("--no-session-persistence")).
  • session_store together with persist_session=False is rejected up front, as TS does (sessionStore cannot be used with persistSession: false), because the store mirrors the transcript from those local writes. The check sits in validate_session_store_options, so query() and ClaudeSDKClient both fail before the CLI is spawned.

Verification

  • Real model calls. CLI 2.1.283 through ClaudeSDKClient with model="haiku", setting_sources=[], a fresh temp cwd: two turns (the first reads a file with the Read tool), then a query(resume=<session_id>).

    • default: projects/-tmp-persist-real-m5d-fb86/09f8511b-5713-4ef1-92d2-76765b91de18.jsonl is written, and the resume answers from the earlier turns
    • persist_session=False: both turns succeed, no file anywhere under ~/.claude names the session, and the resume fails with No conversation found with session ID: a4521e2a-80b8-4019-858a-3192b82a85b8
  • Real CLI, no model calls. The bundled CLI version (2.1.283) driven through query() with an isolated CLAUDE_CONFIG_DIR, cwd in a temp dir, and ANTHROPIC_BASE_URL pointed at a local server that answers 401. The transcript is written before the first API request, so this shows what persists:

    • default: projects/-tmp-persist-probe-work-0in8dcqu/0d5dc4b2-ab19-4c6d-b5ba-b491f5e1ad3b.jsonl is written
    • persist_session=False: no .jsonl is written

    The CLI's --help says this flag "only works with --print". The SDK doesn't pass --print, and the run above shows the flag is honoured in SDK mode anyway.

  • query() and ClaudeSDKClient with session_store=InMemorySessionStore(), persist_session=False both raise the ValueError before spawning.

  • New tests: test_build_command_persist_session (tests/test_transport.py), and test_rejects_persist_session_false_combo and test_persist_session_false_without_store_is_valid (tests/test_session_store_conformance.py). With the src/ changes stashed, all three fail (TypeError: ... unexpected keyword argument 'persist_session'); with them, all three pass.

  • New e2e tests in e2e-tests/test_persist_session.py, laid out like the verbatim_prompts ones: a control run that list_sessions() finds, then query() and ClaudeSDKClient runs with persist_session=False that it doesn't. 3 passed against the real API.

  • ruff check, ruff format --check, mypy src/ scripts/ are clean. pytest tests/: 1590 passed, 6 skipped on Python 3.12 and 3.13, and again with mcp==1.23.0 (the floor job).

The TypeScript SDK's persistSession option has no Python counterpart, so
turning off transcript writes meant reaching for extra_args. With
persist_session=False the CLI runs with --no-session-persistence and saves
nothing under ~/.claude/projects/.

As in the TypeScript SDK, the flag is sent only for an explicit False, and
session_store is rejected alongside it because the store mirrors the
transcript from those local writes. The e2e tests follow the
verbatim_prompts layout: a control run that list_sessions() finds, then
query() and ClaudeSDKClient runs with persist_session=False that it doesn't.
``resume`` unless ``fork_session`` is also set.
"""

persist_session: bool = True

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClaudeAgentOptions is a positional dataclass, so inserting persist_session before max_turns changes the meaning of existing positional constructor calls. A caller that previously supplied max_turns=3 by position now sets persist_session=3 and shifts every following argument, silently changing session behavior. Could this field be appended to the dataclass or made keyword-only to preserve constructor compatibility? This is the same ABI concern as the new field in #1477.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the comment. Same reasoning as on #1477 (#1477 (comment)): I put persist_session beside resume and session_id because new options here go next to related ones rather than at the end, and every ClaudeAgentOptions(...) call in the repo and docs uses keyword arguments. I'll move both fields to the end if the maintainers prefer.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for checking the in-repo usage, @model-bender. My concern is external callers of the public positional ClaudeAgentOptions dataclass, which won't necessarily follow the repository's keyword-only examples. Using field(default=True, kw_only=True) for persist_session would let it stay next to the related session options without shifting existing positional arguments such as max_turns. Could we consider that and add a compatibility test? Happy to defer to the maintainers' preferred public API convention.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, that's a fair point about external callers. The maintainers' own option PRs add fields mid-class without kw_only, for example strict_mcp_config at position 5 of 44 (#915) and verbatim_prompts at 32 of 49 (#1269), so the positional order already changes between releases. I'd rather follow that convention than add the first kw_only field in types.py, but I'm happy to switch, with the test, if a maintainer prefers it.

@sylvesterkaczmarek

Copy link
Copy Markdown

Thanks for checking the existing SDK convention, @model-bender. The examples in #915 and #1269 clarify why you placed the option alongside related fields. I wanted the external positional-caller compatibility concern recorded, but I agree the maintainers should decide whether to keep that convention or introduce a keyword-only field. No need to hold up the feature on my comment. Appreciate your thoughtful follow-up.

@model-bender

model-bender commented Nov 11, 2026 •

Copy link
Copy Markdown
Author

No worries, and thanks @sylvesterkaczmarek, I appreciate you taking the time to look at this and follow up. Having the positional-caller concern recorded here is useful for the maintainers either way.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants