Skip to content

Commit ef60376

Browse files
author
lijiuyang.5137
committed
Improve Claude SDK compatibility
1 parent e99b239 commit ef60376

7 files changed

Lines changed: 1752 additions & 49 deletions

File tree

README.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,7 @@ through the tap.
454454

455455
## Compatibility Matrix
456456

457-
Captured from `claude --help` on Claude Code `2.1.146`.
457+
Captured from `claude --help` on Claude Code `2.1.148`.
458458

459459
Status legend:
460460

@@ -497,8 +497,8 @@ Status legend:
497497
| `--input-format` | Supported | `text` prompts are read from argv/stdin. `stream-json` SDK input is read from stdin. | Fake-PTY test, Python SDK test, TypeScript SDK test, plus live Python/TypeScript SDK game tests. |
498498
| `--json-schema` | Partial | Forwarded, but `cctty` synthesizes result frames when interactive transcript lacks one; `structured_output` parity is not proven. | Parser coverage only. Needs structured-output differential. |
499499
| `--max-budget-usd` | Partial | Forwarded, but Claude documents this as print-only. Underlying interactive behavior is not proven equivalent. | Parser coverage only. |
500-
| `--mcp-config` | Pass-through | Forwarded to interactive Claude. | Parser coverage only. SDK MCP control messages are not bridged yet. |
501-
| `--mcp-debug` | Pass-through | Forwarded to interactive Claude. | Parser coverage only. |
500+
| `--mcp-config` | Supported | Normal Claude-compatible stdio/SSE MCP configs are forwarded to interactive Claude. SDK in-process MCP servers are bridged: TypeScript `initialize.sdkMcpServers` and Python `{ "type": "sdk" }` entries are rewritten into a temporary stdio MCP proxy, and `mcp_message` control requests are round-tripped back to the SDK. | Parser coverage, fake-PTY argv passthrough for normal MCP configs, and TS/Python SDK-MCP round-trip tests covering `initialize`, `tools/list`, and `tools/call`. |
501+
| `--mcp-debug` | Pass-through | Forwarded to interactive Claude. | Parser coverage plus fake-PTY argv passthrough test. |
502502
| `--model` | Pass-through | Forwarded to interactive Claude. | Parser coverage and live differential with default configured model path. Specific model aliases not exhaustively tested. |
503503
| `-n`, `--name` | Pass-through | Forwarded to interactive Claude. | Parser coverage only. |
504504
| `--no-chrome` | Pass-through | Forwarded to interactive Claude. | Parser coverage only. |
@@ -515,7 +515,7 @@ Status legend:
515515
| `--session-id` | Supported | Forwarded and used to locate the transcript. If omitted in print mode, `cctty` creates one. | Fake-PTY and live differential cover session-id based transcript tailing. |
516516
| `--setting-sources` | Pass-through | Forwarded to interactive Claude. | Parser coverage, including `--setting-sources=project`. |
517517
| `--settings` | Pass-through | Forwarded to interactive Claude. | Parser coverage only. |
518-
| `--strict-mcp-config` | Pass-through | Forwarded to interactive Claude. | Parser coverage only. |
518+
| `--strict-mcp-config` | Pass-through | Forwarded to interactive Claude. | Parser coverage plus fake-PTY argv passthrough test. |
519519
| `--system-prompt` | Pass-through | Non-empty values are forwarded to interactive Claude. Empty or whitespace-only SDK values are consumed so they do not erase Claude Code's built-in interactive system prompt. | Parser coverage for non-empty pass-through and empty-value consumption. |
520520
| `--tmux` | Pass-through | Forwarded. `--tmux=classic` is preserved as an equals-form flag; plain `--tmux` does not swallow the prompt. | Parser regression test. |
521521
| `--tools` | Pass-through | Forwarded to interactive Claude. | Parser coverage only. |
@@ -529,10 +529,14 @@ Some SDKs pass flags that are not listed in current `claude --help`.
529529

530530
| Option(s) | Status | Current handling | Notes |
531531
| --- | --- | --- | --- |
532-
| `--permission-prompt-tool stdio` | Partial | Consumed by `cctty`, not forwarded to interactive Claude. In `stream-json` mode, `cctty` watches transcript `assistant.tool_use` entries and also recognizes real TTY permission forms when Claude has not persisted the transcript yet. It emits SDK-style `control_request` / `can_use_tool`, waits for the matching `control_response`, then drives the interactive permission UI by keyboard. Bash allow confirms the selected row; Bash deny selects menu item `2` and pastes the SDK denial message into Claude's follow-up form when present. File create/write/edit/update forms are mapped to `Write`/`Edit` with `file_path`; allow confirms the selected row, while deny selects the file prompt's `3. No` row to avoid accidentally choosing "allow all edits during this session". `AskUserQuestion` form tool uses are forwarded with their original structured `questions` input when available; if only the TTY form is visible, cctty parses that form and sends an SDK-shaped fallback request. SDK-returned structured answers are textified and sent through Claude's follow-up prompt, or submitted as the next user message if Claude returns to the main prompt after cancelling the form. If interactive Claude returns to the prompt after a rejected tool without writing a final result, `cctty` emits a synthetic error result with `result: "Permission denied"`. | Fake-PTY tests cover transcript-first allow/deny, Bash TTY-form-before-transcript, file Write TTY-form-before-transcript, transcript-vs-TTY description precedence, `AskUserQuestion` structured form answer round-trip, and TTY-form-before-transcript `AskUserQuestion` fallback. Live Claude Code `2.1.148` coverage forces `Bash(printf:*)` approval with project-local settings and verifies both allow and deny; live `AskUserQuestion` smoke verifies TTY fallback, answer textification, main-prompt answer injection, and final `result`. Live Python and TypeScript SDK game tests exercise `Write` approval through real file-creation TTY forms. Still partial: broader `Edit`/`MultiEdit` TTY variants, exact `permission_suggestions`, and exact `blocked_path` parity are not complete. |
532+
| `--permission-prompt-tool`, `--permission-prompt-tool stdio` | Partial | `stdio` is consumed by `cctty`, not forwarded to interactive Claude. In `stream-json` mode, `cctty` watches transcript `assistant.tool_use` entries and also recognizes real TTY permission forms when Claude has not persisted the transcript yet. It emits SDK-style `control_request` / `can_use_tool`, waits for the matching `control_response`, then drives the interactive permission UI by keyboard. Bash allow confirms the selected row; Bash deny selects menu item `2` and pastes the SDK denial message into Claude's follow-up form when present. File create/write/edit/update forms are mapped to `Write`/`Edit` with `file_path`; allow confirms the selected row, while deny selects the file prompt's `3. No` row to avoid accidentally choosing "allow all edits during this session". `AskUserQuestion` form tool uses are forwarded with their original structured `questions` input when available; if only the TTY form is visible, cctty parses that form and sends an SDK-shaped fallback request. SDK-returned structured answers are textified and sent through Claude's follow-up prompt, or submitted as the next user message if Claude returns to the main prompt after cancelling the form. If interactive Claude returns to the prompt after a rejected tool without writing a final result, `cctty` emits a synthetic error result with `result: "Permission denied"`. | Fake-PTY tests cover transcript-first allow/deny, Bash TTY-form-before-transcript, file Write TTY-form-before-transcript, transcript-vs-TTY description precedence, `AskUserQuestion` structured form answer round-trip, and TTY-form-before-transcript `AskUserQuestion` fallback. Live Claude Code `2.1.148` coverage forces `Bash(printf:*)` approval with project-local settings and verifies both allow and deny; live `AskUserQuestion` smoke verifies TTY fallback, answer textification, main-prompt answer injection, and final `result`. Live Python and TypeScript SDK game tests exercise `Write` approval through real file-creation TTY forms. Still partial: broader `Edit`/`MultiEdit` TTY variants, exact `permission_suggestions`, and exact `blocked_path` parity are not complete. |
533533
| `--permission-prompt-tool <name>` | Pass-through | Non-`stdio` values are forwarded to interactive Claude. `cctty` does not emulate custom permission prompt tools itself. | Parser coverage only. |
534-
| `--system-prompt-file` | Pass-through | Forwarded to interactive Claude. | Parser coverage because SDKs/older CLIs may emit it. |
535-
| `--task-budget`, `--max-thinking-tokens`, `--thinking`, `--thinking-display`, `--managed-settings`, `--resume-session-at` | Pass-through | Forwarded to interactive Claude. | Parser coverage only. These are SDK/newer-CLI compatibility entries, not from the captured help output above. |
534+
| `--max-turns`, `--task-budget`, `--max-thinking-tokens`, `--thinking`, `--thinking-display` | Pass-through | Forwarded to interactive Claude. These are emitted by official SDK thinking, budget, and turn-limit options. | Parser coverage only. Print-mode parity still belongs to real Claude. |
535+
| `--system-prompt-file`, `--append-system-prompt-file`, `--managed-settings`, `--resume-session-at` | Pass-through | Forwarded to interactive Claude. | Parser coverage only. These are SDK/newer-CLI compatibility entries, not from the captured public help output above. |
536+
| `--session-mirror`, `--sdk-url` | Pass-through | Forwarded to interactive Claude. | Parser coverage only. These are hidden SDK transport/session flags; cctty does not implement the remote SDK transport itself. |
537+
| `--advisor`, `--channels`, `--dangerously-load-development-channels`, `--plan-mode-instructions`, `--plan-mode-required` | Pass-through | Forwarded to interactive Claude. | Parser coverage only. Hidden/native Claude behavior is not differential-tested. |
538+
| `--agent-id`, `--agent-name`, `--team-name`, `--agent-color`, `--agent-type`, `--parent-session-id`, `--teammate-mode` | Pass-through | Forwarded to interactive Claude. | Parser coverage only. These hidden teammate/team flags are treated as native Claude-owned semantics. |
539+
| `--remote`, `--rc`, `--teleport`, `--prefill`, `--prefill-b64`, `--rewind-files`, `--deep-link-cwd-b64`, `--deep-link-last-fetch`, `--deep-link-origin`, `--deep-link-repo`, `--enable-auth-status`, `--enable-auto-mode`, `--init`, `--init-only`, `--maintenance`, `--workload`, `--cowork`, `--xaa` | Pass-through | Forwarded to interactive Claude. | Parser coverage only. These are hidden/native flags found in the installed Claude binary; users should prefer documented flags unless an SDK or host app emits them. |
536540

537541
### Current High-Risk Gaps
538542

src/args.rs

Lines changed: 118 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -309,9 +309,19 @@ fn flag_arity(arg: &str) -> FlagArity {
309309
match flag {
310310
"--add-dir" | "--allowedTools" | "--allowed-tools" | "--betas" | "--disallowedTools"
311311
| "--disallowed-tools" | "--file" | "--mcp-config" | "--tools" => FlagArity::Many,
312+
"--channels" | "--dangerously-load-development-channels" => FlagArity::Many,
312313
"--agent"
314+
| "--advisor"
315+
| "--agent-color"
316+
| "--agent-id"
317+
| "--agent-name"
318+
| "--agent-type"
313319
| "--agents"
314320
| "--append-system-prompt"
321+
| "--append-system-prompt-file"
322+
| "--deep-link-cwd-b64"
323+
| "--deep-link-last-fetch"
324+
| "--deep-link-repo"
315325
| "--debug-file"
316326
| "--effort"
317327
| "--fallback-model"
@@ -323,22 +333,30 @@ fn flag_arity(arg: &str) -> FlagArity {
323333
| "--model"
324334
| "--name"
325335
| "-n"
336+
| "--parent-session-id"
326337
| "--permission-mode"
327338
| "--permission-prompt-tool"
339+
| "--plan-mode-instructions"
328340
| "--plugin-dir"
329341
| "--plugin-url"
342+
| "--prefill"
343+
| "--prefill-b64"
344+
| "--rewind-files"
330345
| "--resume-session-at"
331346
| "--remote-control-session-name-prefix"
347+
| "--sdk-url"
332348
| "--setting-sources"
333349
| "--settings"
334350
| "--system-prompt"
335351
| "--system-prompt-file"
336352
| "--task-budget"
353+
| "--team-name"
354+
| "--teammate-mode"
337355
| "--thinking"
338-
| "--thinking-display" => FlagArity::One,
339-
"--debug" | "-d" | "--from-pr" | "--remote-control" | "--worktree" | "-w" => {
340-
FlagArity::Optional
341-
}
356+
| "--thinking-display"
357+
| "--workload" => FlagArity::One,
358+
"--debug" | "-d" | "--from-pr" | "--rc" | "--remote" | "--remote-control"
359+
| "--teleport" | "--worktree" | "-w" => FlagArity::Optional,
342360
_ => FlagArity::None,
343361
}
344362
}
@@ -497,6 +515,40 @@ mod tests {
497515
}
498516
}
499517

518+
#[test]
519+
fn parse_passes_hidden_sdk_and_native_option_shapes() {
520+
for case in hidden_sdk_and_native_option_cases() {
521+
let mut argv = vec![
522+
"cctty".to_owned(),
523+
"--output-format".to_owned(),
524+
"stream-json".to_owned(),
525+
];
526+
argv.extend(case.argv.iter().map(|value| value.to_string()));
527+
argv.push("--input-format".to_owned());
528+
argv.push("stream-json".to_owned());
529+
530+
let invocation = Invocation::parse(argv)
531+
.unwrap_or_else(|error| panic!("{} failed to parse: {error}", case.name));
532+
assert_eq!(
533+
invocation.input_format,
534+
InputFormat::StreamJson,
535+
"{} swallowed --input-format",
536+
case.name
537+
);
538+
for expected in case.expected_passthrough {
539+
assert!(
540+
invocation
541+
.passthrough_args
542+
.iter()
543+
.any(|arg| arg == expected),
544+
"{} did not pass through {expected:?}; got {:?}",
545+
case.name,
546+
invocation.passthrough_args
547+
);
548+
}
549+
}
550+
}
551+
500552
#[test]
501553
fn parse_tracks_permission_prompt_stdio() {
502554
let invocation = Invocation::parse(vec![
@@ -729,6 +781,68 @@ mod tests {
729781
]
730782
}
731783

784+
fn hidden_sdk_and_native_option_cases() -> Vec<OptionCase> {
785+
vec![
786+
pass("--advisor", &["--advisor", "sonnet"]),
787+
pass("--agent-color", &["--agent-color", "blue"]),
788+
pass("--agent-id", &["--agent-id", "agent_0000000000000000"]),
789+
pass("--agent-name", &["--agent-name", "Test Agent"]),
790+
pass("--agent-type", &["--agent-type", "reviewer"]),
791+
pass(
792+
"--append-system-prompt-file",
793+
&["--append-system-prompt-file", "append.md"],
794+
),
795+
pass("--channels", &["--channels", "server-a", "server-b"]),
796+
pass("--cowork", &["--cowork"]),
797+
pass(
798+
"--dangerously-load-development-channels",
799+
&[
800+
"--dangerously-load-development-channels",
801+
"server-a",
802+
"server-b",
803+
],
804+
),
805+
pass("--deep-link-cwd-b64", &["--deep-link-cwd-b64", "L3RtcA=="]),
806+
pass("--deep-link-last-fetch", &["--deep-link-last-fetch", "0"]),
807+
pass("--deep-link-origin", &["--deep-link-origin"]),
808+
pass("--deep-link-repo", &["--deep-link-repo", "owner/repo"]),
809+
pass("--enable-auth-status", &["--enable-auth-status"]),
810+
pass("--enable-auto-mode", &["--enable-auto-mode"]),
811+
pass("--init", &["--init"]),
812+
pass("--init-only", &["--init-only"]),
813+
pass("--maintenance", &["--maintenance"]),
814+
pass("--managed-settings", &["--managed-settings", "{}"]),
815+
pass("--max-thinking-tokens", &["--max-thinking-tokens", "1024"]),
816+
pass("--max-turns", &["--max-turns", "1"]),
817+
pass("--parent-session-id", &["--parent-session-id", "parent-1"]),
818+
pass(
819+
"--plan-mode-instructions",
820+
&["--plan-mode-instructions", "Write a short plan first"],
821+
),
822+
pass("--plan-mode-required", &["--plan-mode-required"]),
823+
pass("--prefill", &["--prefill", "Draft text"]),
824+
pass("--prefill-b64", &["--prefill-b64", "RHJhZnQ="]),
825+
pass("--rc", &["--rc", "mobile"]),
826+
pass("--remote", &["--remote", "session"]),
827+
pass("--resume-session-at", &["--resume-session-at", "message-1"]),
828+
pass("--rewind-files", &["--rewind-files", "message-1"]),
829+
pass("--sdk-url", &["--sdk-url", "ws://127.0.0.1:1234"]),
830+
pass("--session-mirror", &["--session-mirror"]),
831+
pass(
832+
"--system-prompt-file",
833+
&["--system-prompt-file", "system.md"],
834+
),
835+
pass("--task-budget", &["--task-budget", "2048"]),
836+
pass("--team-name", &["--team-name", "Test Team"]),
837+
pass("--teammate-mode", &["--teammate-mode", "auto"]),
838+
pass("--teleport", &["--teleport", "session-1"]),
839+
pass("--thinking", &["--thinking", "adaptive"]),
840+
pass("--thinking-display", &["--thinking-display", "full"]),
841+
pass("--workload", &["--workload", "test-workload"]),
842+
pass("--xaa", &["--xaa"]),
843+
]
844+
}
845+
732846
fn pass(name: &'static str, argv: &'static [&'static str]) -> OptionCase {
733847
OptionCase {
734848
name,

src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ mod transcript;
88
pub use error::{CcttyError, Result};
99

1010
pub async fn run_cli(argv: Vec<String>) -> Result<i32> {
11+
if argv.get(1).map(String::as_str) == Some("__cctty-mcp-proxy") {
12+
return runner::run_mcp_proxy(argv);
13+
}
14+
1115
let invocation = match args::Invocation::parse(argv) {
1216
Ok(invocation) => invocation,
1317
Err(error) => {

0 commit comments

Comments
 (0)