cdx-manager 0.20.9 → 0.20.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  <br clear="left"/>
6
6
 
7
- [![License](https://img.shields.io/badge/license-MIT-4C8BF5)](LICENSE) ![Version](https://img.shields.io/badge/version-v0.20.9-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
7
+ [![License](https://img.shields.io/badge/license-MIT-4C8BF5)](LICENSE) ![Version](https://img.shields.io/badge/version-v0.20.11-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
8
8
 
9
9
  **Stop guessing which AI account still has quota.** `cdx` tracks the rate-limit window of every Codex and Claude account you own, tells you which one is usable right now, and launches it with isolated auth — in one command.
10
10
 
@@ -106,9 +106,56 @@ The reason `cdx` exists: knowing, across every account you own, which one you ca
106
106
 
107
107
  ### Context handoff
108
108
 
109
- - **Shared handoff context.** Keep a per-workspace Markdown context, or build one from a source session transcript, and install it into another assistant session before switching providers or accounts. Source transcript selection prefers the current workspace when the provider transcript records one, then falls back to the newest source-session transcript.
109
+ - **Transcript-first handoff.** Switch accounts or providers using the source conversation's recorded native identity and verified workspace. CDX never chooses a conversation by file modification time. Missing, stale or mismatched identity requires an explicit choice; JSON/non-interactive callers receive candidate diagnostics instead of a launch.
110
+ - **Instructions before action.** The receiving agent is told to read applicable `AGENTS.md`/`CLAUDE.md` and their required references (including Logics instructions), recover the task from bounded portions of the complete transcript, verify current state, then show a concise objective/state/next-action checkpoint before modifying anything. Existing clear authorization still permits continuation. CDX supplies this prompt; it cannot certify that an agent has understood or obeyed it.
111
+ - **Shared notes remain yours.** `cdx context` and `cdx memory` remain explicit notes. Automatic handoff does not overwrite them or copy authentication. A dated reference to workspace notes is supplementary, never a substitute for the native conversation.
110
112
  - **Session transcript capture.** Every launch is recorded to a local log file via `script`, giving you a full terminal transcript for each session.
111
113
 
114
+ Run handoff from the intended project directory:
115
+
116
+ ```sh
117
+ cd /path/to/project
118
+ cdx handoff source target --json # prepare only, no provider launch
119
+ cdx handoff target # use that prepared entry and launch
120
+ cdx handoff source target --source-conversation CONVERSATION_ID
121
+ # Explicit fallback when only a terminal capture is available:
122
+ cdx handoff source target --source-transcript /path/to/source/log/cdx-session.log
123
+ ```
124
+
125
+ The last two source-selection options are mutually exclusive and apply only to
126
+ `SOURCE TARGET`. Without a usable recorded identity, an interactive terminal offers
127
+ up to 20 valid conversations from the current workspace, ordered by actual event
128
+ time for inspection, with no default choice. Blank input or Ctrl-C cancels. A stale
129
+ recorded identity never silently falls back, even if there is only one candidate.
130
+ Run from the conversation's original workspace if its metadata names another directory.
131
+
132
+ Each preparation writes a private, unique `<target-auth-home>/handoffs/<uuid>.json`
133
+ entry. `context.target_path` in JSON output names that entry; `handoff` contains its
134
+ metadata, including recovery mode, source conversation ID, transcript path, workspace,
135
+ preparation time, readable byte extent and SHA-256. `source_session`/`target_session`
136
+ contain names/providers only. Transcript bodies and credentials are not emitted.
137
+ Unresolved JSON source selection returns `ok: false`, error code
138
+ `handoff_source_unresolved`, a bounded `candidates` array and a nonzero exit status.
139
+
140
+ The original JSONL remains in place, including early decisions, tool calls/results
141
+ and compaction records. Read it in bounded portions up to `extent_bytes`; there is
142
+ no 120000-character tail replacement. An unfinished final JSONL record is excluded
143
+ from that preparation. Later appends do not change its boundary. Reusing an entry
144
+ checks the original bytes: deletion, truncation or rewriting requires a new handoff,
145
+ not a silent downgrade to notes. Keep the original source profile/transcript available
146
+ until recovery completes. No transcript copy or automatic artifact cleanup is performed;
147
+ entries can be removed after use. A small per-workspace/target pointer chooses the most
148
+ recent preparation for the one-name command; concurrent launches retain their own entry
149
+ path and cannot overwrite each other's entry.
150
+
151
+ Legacy one-name handoff without a prepared entry reads workspace notes in `notes-only`
152
+ mode and explicitly reports that native history was not recovered. Terminal fallback
153
+ requires a named file from that source session's launch logs and is labelled `degraded`:
154
+ it cannot prove complete history or tools, and its workspace is operator-selected rather
155
+ than verified from native metadata. Native resolution currently supports Codex and Claude.
156
+ All modes ask the recipient to report material gaps, and none bypass provider access rules
157
+ or turn historical tool output into new authorization.
158
+
112
159
  ### Maintenance
113
160
 
114
161
  - **Launch history.** Inspect recent launches with provider, result, duration, working directory, launch settings, and transcript path.
@@ -515,8 +562,8 @@ cdx history --summary --from 2026-05-01 --to 2026-05-28
515
562
  | `cdx can-resume <name> [--json]` | Check whether a session supports native resume without launching the provider |
516
563
  | `cdx context show\|path\|init\|edit\|clear\|set\|append [text...] [--json]` | Manage the shared Markdown context for the current workspace |
517
564
  | `cdx memory [--global\|--project NAME_OR_PATH] [show\|view\|path\|init\|edit\|clear\|set\|append\|list] [text...] [--json]` | Manage explicit user-controlled memory for the current workspace, global scope, or a named/path project |
518
- | `cdx handoff <name> [--json]` | Install the current workspace context into a target session and launch it unless `--json` is used |
519
- | `cdx handoff <source> <target> [--json]` | Build shared context from the source session's latest launch transcript, install it into the target session, and launch the target unless `--json` is used; supports cross-provider handoff |
565
+ | `cdx handoff <name> [--json]` | Reuse the prepared entry for this target/workspace, or use explicitly labelled notes-only recovery if none exists; launch unless `--json` |
566
+ | `cdx handoff <source> <target> [--source-conversation ID\|--source-transcript PATH] [--json]` | Prepare an identity-checked transcript entry and launch the target in the verified workspace unless `--json`; explicit terminal selection is degraded recovery |
520
567
  | `cdx rmv <name> [--force] [--json]` | Remove a session and its auth data (prompts for confirmation unless `--force`) |
521
568
  | `cdx clean [name] [--yes] [--json]` | Clear launch transcript logs for one session or all sessions after confirmation |
522
569
  | `cdx clean profiles (--tmp\|--old-logs DAYS) [--yes] [--json]` | Remove explicit profile cleanup candidates after confirmation: temporary marketplace/plugin staging caches or old `.log` files |
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.20.9
1
+ 0.20.11
@@ -0,0 +1,71 @@
1
+ # CDX Manager 0.20.10
2
+
3
+ ## Fixes
4
+
5
+ - Keep the local account when `cdx import --merge` brings in a bundle that
6
+ carries authentication. A keychain-backed profile has no credential file, so
7
+ the collision was never noticed and the local login was replaced and its
8
+ keychain entry deleted by an operation whose whole promise is to keep local
9
+ values. The collision is now resolved against the keychain itself, and the
10
+ import says which sessions kept their existing Claude authentication.
11
+ - Restore the destination's Claude credential when `cdx import --force` fails
12
+ after staging the imported one. Filesystem rollback already worked; the
13
+ keychain entry did not come back, which lost the profile's only login. It is
14
+ snapshotted before anything can clear it and restored on failure, and a
15
+ recovery that cannot complete now names Claude keychain authentication
16
+ instead of reporting a generic incomplete recovery.
17
+ - Decode encrypted bundles with the key derivation function their exporter
18
+ recorded. A bundle written on a Python without `hashlib.scrypt` stopped
19
+ opening on a Python that has it, with the same passphrase, because decoding
20
+ re-chose the derivation from the importing runtime. A derivation this runtime
21
+ cannot compute is now reported as such rather than as a wrong passphrase, and
22
+ older bundles that recorded nothing usable stay readable.
23
+ - Keep every accepted note when several agents append to the same `cdx memory`
24
+ scope at once. The append read the scope and wrote it back with nothing
25
+ covering the pair, so both callers were told their note was accepted and one
26
+ of them was not there.
27
+ - Make `cdx run --detach` actually run. The child was asked for a CLI module
28
+ that does not exist, so the launcher reported a run id for work that never
29
+ started and never cleaned up its staged prompt. A module name that does not
30
+ resolve now fails the launch instead of being reported as launched.
31
+ - Terminate the provider when a headless run is interrupted. Ctrl-C unwound the
32
+ supervising CLI and left the provider running in its own process group, still
33
+ consuming quota and writing to the working tree, while the run record
34
+ described the supervisor. The provider tree is now terminated and reaped, and
35
+ the run is recorded as `cancelled` with exit code 130, which a caller can tell
36
+ apart from a failure and from a timeout.
37
+ - Keep a working tray when replacing the companion fails midway. A filesystem
38
+ error between retiring the installed companion and promoting the staged one
39
+ left the install record pointing at a path with nothing in it, and skipped the
40
+ restart of the tray that had just been stopped. The proven companion is put
41
+ back and the tray is restarted on it.
42
+ - Keep the Windows Start Menu shortcut pointing at the companion that is
43
+ actually installed. It was written against the staged executable, which the
44
+ update then moved, and the promoted install record dropped it, so uninstall
45
+ stopped removing it. It is now written by the promotion against the final
46
+ path and stays owned by uninstall.
47
+ - Make the macOS pre-promotion launch check real. `--print` was handed to the
48
+ `open` utility rather than to the companion and the exit status was ignored,
49
+ so the gate passed for an app bundle that never started. It now runs the
50
+ binary inside the bundle, refuses a bundle with nothing runnable in it, and
51
+ still accepts a companion that started and reported a problem of its own.
52
+ - Open the intended command when a Windows tray reaches CDX through WSL.
53
+ A session's launch settings row sent `config <session>` as a single argument,
54
+ so CDX saw one session name with a space in it and refused it.
55
+ - Offer only terminals a Windows tray can open. The candidate list is built by
56
+ whichever host answers the poll, which under WSL is Linux, so the menu could
57
+ offer a Linux terminal and then open nothing when it was chosen. The
58
+ companion now lists what it can launch, and a stored preference it cannot
59
+ open falls back to the default console instead of an inert click.
60
+ - Run the selected command when the tray terminal preference is `cmd`. It was
61
+ given PowerShell's `-NoExit -Command`, which `cmd.exe` accepts without
62
+ running anything, so the click opened an idle shell.
63
+
64
+ ## Notes
65
+
66
+ - A cancelled headless run is a new terminal state: `cancelled`, alongside
67
+ `succeeded`, `failed` and `timed_out`. Callers that branch on run status
68
+ should treat it as an outcome rather than as an unknown value.
69
+ - Bundles exported by this version record and honour their key derivation
70
+ function. Bundles exported by earlier versions are unaffected and stay
71
+ readable.
@@ -0,0 +1,37 @@
1
+ # CDX Manager 0.20.11
2
+
3
+ ## Reliable handoff recovery
4
+
5
+ - Resolve Codex and Claude handoffs using the recorded native conversation ID and
6
+ verify the workspace. An old transcript touched recently can no longer silently
7
+ replace the intended conversation. Missing or stale identities return candidates;
8
+ use `--source-conversation ID` to choose explicitly.
9
+ - Give the receiving agent access to the complete native transcript, including
10
+ earlier decisions and tool evidence, instead of a flattened 120000-character tail.
11
+ Each handoff has its own private entry with provenance, a preparation boundary and
12
+ a checksum. Reusing an entry detects a deleted or rewritten source.
13
+ - Require the receiving agent to read repository instructions and their references,
14
+ recover user decisions and authorization limits, verify current state, and show a
15
+ concise recovery checkpoint before making changes. This is a prompt contract, not
16
+ a guarantee of agent comprehension.
17
+ - Preserve workspace notes as supplementary context rather than overwriting them
18
+ during handoff. Concurrent preparations retain separate entries; the target starts
19
+ in the verified workspace.
20
+
21
+ ## Compatibility notes
22
+
23
+ - `cdx handoff SOURCE TARGET --json` prepares without launching. JSON includes the
24
+ entry path and source metadata, not transcript bodies or authentication data.
25
+ - `cdx handoff TARGET` reuses the prepared entry for that target and workspace. With
26
+ no prepared entry, existing workspace notes remain available as explicitly labelled
27
+ notes-only recovery.
28
+ - Automatic newest-log fallback is removed. For an intentional terminal-only
29
+ fallback, pass `--source-transcript PATH` naming a source session launch log. This
30
+ degraded mode cannot verify complete native history or the original workspace.
31
+ - Keep the original transcript available until recovery completes. The entry refers
32
+ to it in place; it is not a transcript backup.
33
+
34
+ ## Validation
35
+
36
+ - 1048 Python tests pass, including synthetic handoff regressions and mocked launches.
37
+ - Project lint and Logics corpus validation pass.
@@ -408,6 +408,28 @@
408
408
  "x86_64-pc-windows-msvc": "343245a6ce52275e8e7bd9a380d9e03fd1db3bcf917df3e6cecde2d0be1bfaeb",
409
409
  "x86_64-unknown-linux-musl": "5d6e776b5ce64a062c798c5c00f9e85e003b5b6888cece74173bff1f45247f34"
410
410
  }
411
+ },
412
+ "v0.20.9": {
413
+ "github_tarball_sha256": "1d1989861ba26df76ee3b1d0db98fbc2bbacbeb36277411c45f94ea1fddc0996",
414
+ "github_zip_sha256": "997e304f71c69bb22c045e29a76313ba2e0fa0227020cd16b2416aee0d64482d",
415
+ "tray_assets": {
416
+ "aarch64-apple-darwin": "6e07850c91915d5f2c73d6ae5d44c9ccf9ef149f0a63268a9773f1bf215d4d50",
417
+ "aarch64-unknown-linux-musl": "d12869b53457de872054ee0edd4822a4bb53325700ee4b58c30a9c777d58a3b9",
418
+ "x86_64-apple-darwin": "e84c651fa3b20fc99f9d9cd71378a526089292a4122e111bde014159de046b31",
419
+ "x86_64-pc-windows-msvc": "53e6811d5c0489a6f08ec29d65bdbd5a1375aef64fc30ad72bc7e26d38ffbb6d",
420
+ "x86_64-unknown-linux-musl": "d35d02bc18ba80413858d8b9943aa24f6424544c4b6e65770f1dce722d346547"
421
+ }
422
+ },
423
+ "v0.20.10": {
424
+ "github_tarball_sha256": "2b97c8dedc4740d89b8dab5b4ea54c986d211efdf2f9d6c8cbcf014a42bc7eb2",
425
+ "github_zip_sha256": "144cb5987498a96d78299c1589735b5aef5dd31cd3abfe638c541e83464055b5",
426
+ "tray_assets": {
427
+ "aarch64-apple-darwin": "7f89f616c707bfef9f2a0fdccae6e37c22d3b2fb51f6ea2cb5cd5dc8f86446fe",
428
+ "aarch64-unknown-linux-musl": "4295fb3fd27abacae614360783b704e87d8390023843a32e1626ce986004078f",
429
+ "x86_64-apple-darwin": "f68422c8da7316ba575579cf689056b58c23213717203a8ea6c48e26337ae2e7",
430
+ "x86_64-pc-windows-msvc": "ae8eba39b1d9d7e9b97c3b3026561360a6ec00ca154b5d4d215a283fc5566050",
431
+ "x86_64-unknown-linux-musl": "cfa4245e7704a4c85e4f680275db500c600e46a44aac19d1ec5675deb2be0c58"
432
+ }
411
433
  }
412
434
  }
413
435
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdx-manager",
3
- "version": "0.20.9",
3
+ "version": "0.20.11",
4
4
  "description": "Terminal session manager for Codex and Claude accounts.",
5
5
  "license": "MIT",
6
6
  "author": "Alexandre Agostini",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cdx-manager"
7
- version = "0.20.9"
7
+ version = "0.20.11"
8
8
  description = "Terminal session manager for Codex and Claude accounts."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -17,6 +17,9 @@ _SCRYPT_P = 1
17
17
  _PBKDF2_ITERATIONS = 200000
18
18
  _AEAD_ALGORITHM = "aes-256-gcm"
19
19
  _LEGACY_ALGORITHM = "sha256-xor-hmac"
20
+ _KDF_SCRYPT = "scrypt"
21
+ _KDF_PBKDF2 = "pbkdf2-hmac-sha256"
22
+ _SUPPORTED_KDFS = (_KDF_SCRYPT, _KDF_PBKDF2)
20
23
 
21
24
 
22
25
  def _now_iso():
@@ -47,13 +50,42 @@ def read_bundle_meta(data):
47
50
  return wrapper
48
51
 
49
52
 
50
- def _derive_keys(passphrase, salt):
51
- if not passphrase:
52
- raise CdxError("A non-empty passphrase is required for bundles that include auth data.")
53
- if isinstance(passphrase, str):
54
- passphrase = passphrase.encode("utf-8")
55
- if hasattr(hashlib, "scrypt"):
56
- key_material = hashlib.scrypt(
53
+ def _available_kdf(kdf):
54
+ return kdf != _KDF_SCRYPT or hasattr(hashlib, "scrypt")
55
+
56
+
57
+ def _default_kdf():
58
+ """The strongest KDF this runtime can compute. Recorded in the bundle."""
59
+ return _KDF_SCRYPT if _available_kdf(_KDF_SCRYPT) else _KDF_PBKDF2
60
+
61
+
62
+ def _bundle_kdf_candidates(wrapper):
63
+ """KDFs to try when decoding, in order.
64
+
65
+ A recorded KDF is authoritative: the importing runtime's own capabilities
66
+ must not silently select a different derivation, or a portable bundle stops
67
+ decoding after a move between machines. Bundles written before the field was
68
+ honoured record nothing reliable, so those keep the compatibility behaviour
69
+ of trying every KDF this runtime can compute.
70
+ """
71
+ recorded = wrapper.get("kdf")
72
+ if recorded is None:
73
+ default = _default_kdf()
74
+ ordered = [default, *(kdf for kdf in _SUPPORTED_KDFS if kdf != default)]
75
+ return [kdf for kdf in ordered if _available_kdf(kdf)]
76
+ if not isinstance(recorded, str) or recorded not in _SUPPORTED_KDFS:
77
+ raise CdxError("Unsupported bundle key derivation function.")
78
+ return [recorded]
79
+
80
+
81
+ def _derive_key_material(passphrase, salt, kdf):
82
+ if kdf == _KDF_SCRYPT:
83
+ if not _available_kdf(_KDF_SCRYPT):
84
+ raise CdxError(
85
+ "This bundle records the scrypt key derivation function, which this Python runtime "
86
+ "does not provide. Decode it with a Python built against OpenSSL scrypt support."
87
+ )
88
+ return hashlib.scrypt(
57
89
  passphrase,
58
90
  salt=salt,
59
91
  n=_SCRYPT_N,
@@ -61,19 +93,28 @@ def _derive_keys(passphrase, salt):
61
93
  p=_SCRYPT_P,
62
94
  dklen=64,
63
95
  )
64
- else:
65
- key_material = hashlib.pbkdf2_hmac(
96
+ if kdf == _KDF_PBKDF2:
97
+ return hashlib.pbkdf2_hmac(
66
98
  "sha256",
67
99
  passphrase,
68
100
  salt,
69
101
  _PBKDF2_ITERATIONS,
70
102
  dklen=64,
71
103
  )
104
+ raise CdxError("Unsupported bundle key derivation function.")
105
+
106
+
107
+ def _derive_keys(passphrase, salt, kdf=None):
108
+ if not passphrase:
109
+ raise CdxError("A non-empty passphrase is required for bundles that include auth data.")
110
+ if isinstance(passphrase, str):
111
+ passphrase = passphrase.encode("utf-8")
112
+ key_material = _derive_key_material(passphrase, salt, kdf or _default_kdf())
72
113
  return key_material[:32], key_material[32:]
73
114
 
74
115
 
75
- def _derive_aead_key(passphrase, salt):
76
- enc_key, _mac_key = _derive_keys(passphrase, salt)
116
+ def _derive_aead_key(passphrase, salt, kdf=None):
117
+ enc_key, _mac_key = _derive_keys(passphrase, salt, kdf)
77
118
  return enc_key
78
119
 
79
120
 
@@ -110,11 +151,12 @@ def encode_bundle(payload, include_auth=False, passphrase=None):
110
151
  if include_auth:
111
152
  salt = os.urandom(_SALT_BYTES)
112
153
  nonce = os.urandom(_AEAD_NONCE_BYTES)
113
- aesgcm = _load_aesgcm()(_derive_aead_key(passphrase, salt))
154
+ kdf = _default_kdf()
155
+ aesgcm = _load_aesgcm()(_derive_aead_key(passphrase, salt, kdf))
114
156
  ciphertext = aesgcm.encrypt(nonce, payload_bytes, None)
115
157
  wrapper.update({
116
158
  "encryption": _AEAD_ALGORITHM,
117
- "kdf": "scrypt" if hasattr(hashlib, "scrypt") else "pbkdf2-hmac-sha256",
159
+ "kdf": kdf,
118
160
  "salt": _b64_encode(salt),
119
161
  "nonce": _b64_encode(nonce),
120
162
  "payload": _b64_encode(ciphertext),
@@ -138,19 +180,33 @@ def decode_bundle(data, passphrase=None):
138
180
  nonce = _b64_decode(wrapper.get("nonce", ""))
139
181
  ciphertext = _b64_decode(payload_b64)
140
182
  algorithm = wrapper.get("encryption") or _LEGACY_ALGORITHM
183
+ kdf_candidates = _bundle_kdf_candidates(wrapper)
141
184
  if algorithm == _AEAD_ALGORITHM:
142
- aesgcm = _load_aesgcm()(_derive_aead_key(passphrase, salt))
143
- try:
144
- payload_bytes = aesgcm.decrypt(nonce, ciphertext, None)
145
- except Exception as error:
146
- raise CdxError("Invalid bundle passphrase or corrupted bundle.") from error
185
+ aead = _load_aesgcm()
186
+ payload_bytes = None
187
+ failure = None
188
+ for kdf in kdf_candidates:
189
+ # Derivation errors are about the bundle, not the passphrase, so
190
+ # they must escape rather than be retried as a bad secret.
191
+ cipher = aead(_derive_aead_key(passphrase, salt, kdf))
192
+ try:
193
+ payload_bytes = cipher.decrypt(nonce, ciphertext, None)
194
+ break
195
+ except Exception as error: # a wrong KDF looks exactly like a wrong passphrase.
196
+ failure = error
197
+ if payload_bytes is None:
198
+ raise CdxError("Invalid bundle passphrase or corrupted bundle.") from failure
147
199
  elif algorithm == _LEGACY_ALGORITHM:
148
200
  expected_mac = _b64_decode(wrapper.get("hmac_sha256", ""))
149
- enc_key, mac_key = _derive_keys(passphrase, salt)
150
- actual_mac = hmac.new(mac_key, nonce + ciphertext, hashlib.sha256).digest()
151
- if not hmac.compare_digest(actual_mac, expected_mac):
201
+ payload_bytes = None
202
+ for kdf in kdf_candidates:
203
+ enc_key, mac_key = _derive_keys(passphrase, salt, kdf)
204
+ actual_mac = hmac.new(mac_key, nonce + ciphertext, hashlib.sha256).digest()
205
+ if hmac.compare_digest(actual_mac, expected_mac):
206
+ payload_bytes = _xor_keystream(ciphertext, enc_key, nonce)
207
+ break
208
+ if payload_bytes is None:
152
209
  raise CdxError("Invalid bundle passphrase or corrupted bundle.")
153
- payload_bytes = _xor_keystream(ciphertext, enc_key, nonce)
154
210
  else:
155
211
  raise CdxError("Unsupported bundle encryption algorithm.")
156
212
  else:
@@ -91,6 +91,32 @@ def _read_claude_credentials(auth_home):
91
91
  return None
92
92
 
93
93
 
94
+ def claude_profile_store_dir(auth_home):
95
+ """Where `ANTHROPIC_CONFIG_DIR` points: the credential directory, not the home.
96
+
97
+ Claude Code treats `ANTHROPIC_CONFIG_DIR` as the root of its profile store
98
+ and refuses every write beneath it, so whatever this returns is denied to
99
+ the agent's own file-writing tools. Pointing it at `auth_home` denied the
100
+ entire profile home -- `.claude/projects/*/memory`, `.claude/settings.json`,
101
+ `.npm`, `.cargo` -- which is far more than a sign-in store and cost agents
102
+ their memory and settings with no gain.
103
+
104
+ No gain, because the variable does not locate credentials: measured on
105
+ Claude Code 2.1.268, `HOME` alone authenticates and this variable alone does
106
+ not. It only draws the deny boundary, so it belongs on the directory that
107
+ actually holds a credential -- `_write_claude_oauth_token` puts the
108
+ setup-token fallback in `auth_home/credentials/default.json`.
109
+
110
+ Not covered, and deliberately: `auth_home/.claude/.credentials.json`, which
111
+ Claude Code writes where no OS keyring answers. It cannot be: it sits under
112
+ `.claude` beside the memory and settings this narrowing exists to free, and
113
+ `_secure_storage_overrides` explains why that directory cannot move. It
114
+ keeps its 0600 mode; on macOS it does not exist at all, the login keychain
115
+ holding the credential instead.
116
+ """
117
+ return os.path.join(auth_home, "credentials")
118
+
119
+
94
120
  def _link_macos_keychain(auth_home):
95
121
  """Make the operator's login keychain reachable from a redirected home.
96
122
 
@@ -148,7 +174,7 @@ def _secure_storage_overrides(auth_home):
148
174
  def _home_env_overrides(auth_home):
149
175
  overrides = {
150
176
  "HOME": auth_home,
151
- "ANTHROPIC_CONFIG_DIR": auth_home,
177
+ "ANTHROPIC_CONFIG_DIR": claude_profile_store_dir(auth_home),
152
178
  "CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS": "1",
153
179
  }
154
180
  if sys.platform == "win32":
package/src/cli.py CHANGED
@@ -260,7 +260,7 @@ def _print_help(use_color=False):
260
260
  f" {_style('cdx resume <name> [--json]', '36', use_color)}",
261
261
  f" {_style('cdx can-resume <name> [--json]', '36', use_color)}",
262
262
  f" {_style('cdx handoff <name> [--json]', '36', use_color)}",
263
- f" {_style('cdx handoff <source> <target> [--json]', '36', use_color)}",
263
+ f" {_style('cdx handoff <source> <target> [--source-conversation ID | --source-transcript PATH] [--json]', '36', use_color)}",
264
264
  f" {_style('cdx add [provider] <name> [--model MODEL] [--json]', '36', use_color)}",
265
265
  f" {_style('cdx cp <source> <dest> [--json]', '36', use_color)}",
266
266
  f" {_style('cdx ren <source> <dest> [--json]', '36', use_color)}",
package/src/cli_args.py CHANGED
@@ -30,7 +30,7 @@ EXPORT_USAGE = "Usage: cdx export <file> [--include-auth] [--force] [--json] [--
30
30
  IMPORT_USAGE = "Usage: cdx import <file> [--force|--merge] [--allow-authless-force] [--json] [--sessions name1,name2] [--passphrase-env VAR|--passphrase-stdin]"
31
31
  CONTEXT_USAGE = "Usage: cdx context show|path|init|edit|clear|set|append [text...] [--json]"
32
32
  MEMORY_USAGE = "Usage: cdx memory [--global|--project NAME_OR_PATH] [show|view|path|init|edit|clear|set|append|list] [text...] [--json]"
33
- HANDOFF_USAGE = "Usage: cdx handoff <name> [--json] | cdx handoff <source> <target> [--json]"
33
+ HANDOFF_USAGE = "Usage: cdx handoff <name> [--json] | cdx handoff <source> <target> [--source-conversation ID | --source-transcript PATH] [--json]"
34
34
  LABEL_USAGE = "Usage: cdx label <name> <label> [--json] | cdx label <name> --clear [--json]"
35
35
  SET_USAGE = "Usage: cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power minimal|low|medium|high|xhigh] [--permission review|default|auto|full] [--fast on|off] [--rtk on|off] [--logics on|off] [--model MODEL] [--fallback-model MODEL[,MODEL...]] [--budget USD] [--extra-args 'ARGS'] [--priority 0..100] [--json]"
36
36
  UNSET_USAGE = "Usage: cdx unset <name>|--sessions all|a,b|--provider PROVIDER (--power|--reasoning-effort|--permission|--fast|--rtk|--logics|--model|--fallback-model|--budget|--extra-args|--priority|--all) [--json]"
@@ -15,20 +15,16 @@ from .cli_args import STATUS_USAGE # noqa: F401 (re-exported for cli.py)
15
15
  from .cli_helpers import ( # noqa: F401 (several names are re-exported for cli.py / tests)
16
16
  API_SCHEMA_VERSION,
17
17
  _bootstrap_claude_setup_token,
18
- _build_handoff_context,
19
18
  _extract_claude_oauth_token,
20
19
  _format_bytes,
21
20
  _format_launch_config,
22
21
  _format_launch_setting_value,
23
22
  _format_launch_settings_hint,
24
- _handoff_launch_prompt,
25
23
  _json_failure,
26
24
  _json_success,
27
- _latest_handoff_transcript_path,
28
25
  _local_now_iso,
29
26
  _make_notify_progress,
30
27
  _make_status_progress,
31
- _read_handoff_transcript,
32
28
  _resolve_confirmation,
33
29
  _resume_capability_for_session,
34
30
  _update_notice_warnings,