Inject MAX_MCP_OUTPUT_TOKENS into AutoSkillit Order Sessions#910
Merged
Trecek merged 5 commits intointegrationfrom Apr 14, 2026
Merged
Conversation
Raises the Claude Code client-side MCP tool result size gate from 25K to 50K tokens in every AutoSkillit-launched session, preventing open_kitchen() responses (~27K tokens) from being persisted to a file instead of returned inline. - Add MAX_MCP_OUTPUT_TOKENS to IDE_ENV_DENYLIST in core/_claude_env.py - Add _MAX_MCP_OUTPUT_TOKENS_VALUE constant and extend _HEADLESS_EXCLUSIVE_VARS in execution/commands.py - Inject the constant into build_full_headless_cmd extras dict - Pass env_extras carrying the constant in cli/_cook.py cook() - Add tests for denylist coverage, headless injection, exclusive-var stripping, and cook session injection Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… avoid submodule import in _cook.py
Trecek
commented
Apr 14, 2026
Collaborator
Author
Trecek
left a comment
There was a problem hiding this comment.
AutoSkillit PR Review — Verdict: changes_requested (5 actionable findings)
| # Raises the Claude Code client-side MCP tool result size gate from the | ||
| # default 25,000 tokens to 50,000, preventing open_kitchen() responses | ||
| # from being persisted to a file instead of returned inline. | ||
| _MAX_MCP_OUTPUT_TOKENS_VALUE: str = "50000" |
Collaborator
Author
There was a problem hiding this comment.
[warning] defense/cohesion: _MAX_MCP_OUTPUT_TOKENS_VALUE is underscore-prefixed (private) but intentionally re-exported via execution/__init__.py. Consider removing the leading underscore to match its public surface contract (MAX_MCP_OUTPUT_TOKENS_VALUE).
Collaborator
Author
There was a problem hiding this comment.
Valid observation — flagged for design decision. The underscore prefix is consistent with other module-private-but-re-exported symbols in execution/__init__.py (e.g. _execute_readonly_query, _refresh_quota_cache). Renaming would touch 5 locations. Deferring to a separate refactor.
…OKENS_VALUE re-export The inline comment named _cook.py as the sole consumer, embedding coupling rationale into the re-export layer. The noqa: F401 alone is sufficient to signal re-export intent.
…assertions Replaced hardcoded "50000" literals with the imported constant in test_cook_env_scrub.py and test_commands.py so that a value change in the constant is automatically caught by these tests.
This was referenced Apr 14, 2026
github-merge-queue bot
pushed a commit
that referenced
this pull request
Apr 14, 2026
…ypes (#922) ## Summary `build_interactive_cmd()` is a pure pass-through for environment variables — it forwards caller-supplied `env_extras` to `build_claude_env` without injecting any defaults. This is structurally asymmetric with `build_full_headless_cmd()`, which internally injects `MAX_MCP_OUTPUT_TOKENS=50000` and `AUTOSKILLIT_HEADLESS=1` before delegating. The asymmetry means interactive launch paths depend on caller discipline to inject required vars, and when `_launch_cook_session` (the `order` command's launch path) was never updated by PR #910, the gap went undetected. The fix adds a `_SESSION_BASELINE_ENV` frozen mapping in `commands.py` that `build_interactive_cmd` and `build_headless_resume_cmd` merge as defaults. Removes redundant caller-side injection from `_cook.py`. Adds structural guard tests covering all three session builders. Closes #916 ## Implementation Plan Plan file: `/home/talon/projects/autoskillit-runs/remediation-20260414-084428-773530/.autoskillit/temp/rectify/rectify_max_mcp_output_tokens_interactive_gap_2026-04-14_090500.md` 🤖 Generated with [Claude Code](https://claude.com/claude-code) via AutoSkillit ## Token Usage Summary | Step | uncached | output | cache_read | cache_write | count | time | |------|----------|--------|------------|-------------|-------|------| | investigate | 2.6k | 8.3k | 628.8k | 48.1k | 1 | 4m 55s | | rectify | 4.1k | 26.1k | 1.8M | 161.5k | 3 | 12m 42s | | dry_walkthrough | 3.2k | 10.1k | 1.2M | 78.6k | 1 | 4m 39s | | implement | 77 | 10.5k | 1.6M | 54.7k | 1 | 4m 19s | | prepare_pr | 30 | 4.5k | 374.0k | 34.2k | 1 | 1m 34s | | run_arch_lenses | 49 | 8.2k | 538.5k | 47.7k | 1 | 2m 52s | | compose_pr | 23 | 1.5k | 128.6k | 15.6k | 1 | 39s | | **Total** | 10.1k | 69.2k | 6.3M | 440.6k | | 31m 42s | Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Claude Code v2.1.92 added a client-side MCP tool result size gate (default 25,000 tokens). The
open_kitchen(name="implementation")response now exceeds this threshold (~80K chars / ~25-27K tokens), causing Claude Code to persist the result to a file and return an error instead — breaking orchestration flow.The fix injects
MAX_MCP_OUTPUT_TOKENS=50000into every subprocess environment that AutoSkillit launches (headless sessions and cook sessions). Simultaneously, the variable is added to the denylist in_claude_env.pyand to_HEADLESS_EXCLUSIVE_VARSincommands.pyso it cannot leak from a parent session into children (children always get it set explicitly). A named constant_MAX_MCP_OUTPUT_TOKENS_VALUE = "50000"incommands.pyavoids magic-number duplication and is re-exported viaexecution/__init__.pyfor use in_cook.py.Architecture Impact
Security Diagram
%%{init: {'flowchart': {'nodeSpacing': 45, 'rankSpacing': 55, 'curve': 'basis'}}}%% flowchart TB %% CLASS DEFINITIONS %% classDef cli fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff; classDef stateNode fill:#004d40,stroke:#4db6ac,stroke-width:2px,color:#fff; classDef handler fill:#e65100,stroke:#ffb74d,stroke-width:2px,color:#fff; classDef phase fill:#6a1b9a,stroke:#ba68c8,stroke-width:2px,color:#fff; classDef newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff; classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff; classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff; classDef gap fill:#ff6f00,stroke:#ffa726,stroke-width:2px,color:#000; classDef terminal fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff; %% ENTRY %% HOSTENV(["Host os.environ<br/>━━━━━━━━━━<br/>Untrusted / IDE-polluted"]) subgraph DenylistLayer ["● TRUST BOUNDARY 1 — IDE Env Scrub (core/_claude_env.py)"] direction TB EXACTDENY["● IDE_ENV_DENYLIST<br/>━━━━━━━━━━<br/>Exact-match strip:<br/>CLAUDE_CODE_SSE_PORT<br/>ENABLE_IDE_INTEGRATION<br/>CLAUDE_CODE_WEBSOCKET_AUTH_FD<br/>VSCODE_GIT_ASKPASS_MAIN<br/>CURSOR_TRACE_ID · ZED_TERM<br/>EXIT_AFTER_STOP_DELAY ●<br/>SCENARIO_STEP_NAME ●<br/>MAX_MCP_OUTPUT_TOKENS ●"] PREFIXDENY["● IDE_ENV_PREFIX_DENYLIST<br/>━━━━━━━━━━<br/>Prefix-match strip:<br/>CLAUDE_CODE_IDE_*<br/>CLAUDE_CODE_SSE*"] AUTOCONN["● IDE_ENV_ALWAYS_EXTRAS<br/>━━━━━━━━━━<br/>Always injected:<br/>CLAUDE_CODE_AUTO_CONNECT_IDE=0<br/>(closes lock-file scan fallback)"] end subgraph ExclusiveFilter ["● TRUST BOUNDARY 2 — Headless Exclusive Var Filter (execution/commands.py)"] direction TB EXCL["● _HEADLESS_EXCLUSIVE_VARS filter<br/>━━━━━━━━━━<br/>Strips from os.environ before<br/>build_headless_cmd() base arg:<br/>EXIT_AFTER_STOP_DELAY<br/>SCENARIO_STEP_NAME<br/>MAX_MCP_OUTPUT_TOKENS"] end subgraph ExplicitInject ["● TRUST BOUNDARY 3 — Controlled Explicit Injection (execution/commands.py)"] direction TB MCPTOK["● MAX_MCP_OUTPUT_TOKENS=50000<br/>━━━━━━━━━━<br/>_MAX_MCP_OUTPUT_TOKENS_VALUE<br/>Always injected into headless extras<br/>Raises MCP gate: 25k → 50k tokens"] HEADLESSFLAGS["AUTOSKILLIT_HEADLESS=1<br/>━━━━━━━━━━<br/>Headless-mode signal<br/>Injected unconditionally"] OPTIONALS["Optional caller-controlled:<br/>━━━━━━━━━━<br/>EXIT_AFTER_STOP_DELAY (ms > 0)<br/>SCENARIO_STEP_NAME (non-empty)"] end subgraph SealedEnv ["SEALED ENV — MappingProxyType (core/_claude_env.py → build_claude_env)"] SEALED["MappingProxyType<br/>━━━━━━━━━━<br/>Read-only view<br/>Post-build mutation blocked"] end subgraph HeadlessPath ["● Headless Session Path (execution/commands.py → build_full_headless_cmd)"] HEADLESSCMD["● ClaudeHeadlessCmd<br/>━━━━━━━━━━<br/>cmd + sealed env<br/>Ready for subprocess runner"] end subgraph CookPath ["● Cook (Interactive) Session Path (cli/_cook.py)"] COOKEXTRAS["● env_extras injection<br/>━━━━━━━━━━<br/>MAX_MCP_OUTPUT_TOKENS=50000<br/>via _MAX_MCP_OUTPUT_TOKENS_VALUE<br/>(imported from execution/__init__.py ●)"] COOKCMD["ClaudeInteractiveCmd<br/>━━━━━━━━━━<br/>cmd + sealed env<br/>subprocess.run(env=spec.env)"] end BLOCKED(["BLOCKED<br/>━━━━━━━━━━<br/>IDE env vars<br/>Leaked session vars"]) %% FLOW %% HOSTENV --> EXACTDENY HOSTENV --> PREFIXDENY EXACTDENY -->|"stripped"| BLOCKED PREFIXDENY -->|"stripped"| BLOCKED EXACTDENY -->|"surviving vars"| AUTOCONN PREFIXDENY -->|"surviving vars"| AUTOCONN AUTOCONN -->|"base env"| EXCL EXCL -->|"headless path"| MCPTOK EXCL -->|"headless path"| HEADLESSFLAGS EXCL -->|"headless path"| OPTIONALS MCPTOK --> SEALED HEADLESSFLAGS --> SEALED OPTIONALS --> SEALED AUTOCONN -->|"cook path"| COOKEXTRAS SEALED --> HEADLESSCMD COOKEXTRAS --> COOKCMD %% CLASS ASSIGNMENTS %% class HOSTENV cli; class EXACTDENY,PREFIXDENY detector; class AUTOCONN,EXCL detector; class MCPTOK,HEADLESSFLAGS phase; class OPTIONALS stateNode; class SEALED output; class HEADLESSCMD,COOKCMD output; class COOKEXTRAS phase; class BLOCKED gap;Process Flow Diagram
%%{init: {'flowchart': {'nodeSpacing': 45, 'rankSpacing': 55, 'curve': 'basis'}}}%% flowchart TB %% CLASS DEFINITIONS %% classDef terminal fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff; classDef stateNode fill:#004d40,stroke:#4db6ac,stroke-width:2px,color:#fff; classDef handler fill:#e65100,stroke:#ffb74d,stroke-width:2px,color:#fff; classDef phase fill:#6a1b9a,stroke:#ba68c8,stroke-width:2px,color:#fff; classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff; classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff; %% TERMINALS %% START([START]) DONE([DONE — ClaudeHeadlessCmd / ClaudeInteractiveCmd]) %% ── ENTRY SPLIT ─────────────────────────────────────────── %% ENTRY{"Session<br/>type?"} START --> ENTRY %% ══════════════════════════════════════════════════════════ %% %% HEADLESS PATH %% %% ══════════════════════════════════════════════════════════ %% subgraph HeadlessPath ["Headless Session Path (execution/commands.py ●)"] direction TB BFH["● build_full_headless_cmd()<br/>━━━━━━━━━━<br/>Entry: skill_command, cwd,<br/>completion_marker, model, plugin_dir,<br/>exit_after_stop_delay_ms, scenario_step_name"] FILTER["● Filter os.environ<br/>━━━━━━━━━━<br/>Remove _HEADLESS_EXCLUSIVE_VARS:<br/>CLAUDE_CODE_EXIT_AFTER_STOP_DELAY<br/>SCENARIO_STEP_NAME<br/>MAX_MCP_OUTPUT_TOKENS<br/>→ filtered_base"] EXTRAS_H["● Assemble headless extras<br/>━━━━━━━━━━<br/>AUTOSKILLIT_HEADLESS = 1<br/>MAX_MCP_OUTPUT_TOKENS = 50000 ← new<br/>+ conditional vars below"] OPT_DELAY{"exit_after_stop<br/>_delay_ms > 0?"} OPT_STEP{"scenario_step<br/>_name set?"} INJECT_DELAY["Add CLAUDE_CODE_EXIT_AFTER_STOP_DELAY"] INJECT_STEP["Add SCENARIO_STEP_NAME"] PROMPT["Build prompt string<br/>━━━━━━━━━━<br/>_ensure_skill_prefix()<br/>→ _inject_completion_directive()<br/>→ _inject_cwd_anchor()<br/>→ _inject_narration_suppression()"] BHC["build_headless_cmd()<br/>━━━━━━━━━━<br/>cmd: [claude, --print, prompt,<br/>--dangerously-skip-permissions, ...]<br/>env: build_claude_env(base=filtered_base,<br/> extras=extras)"] end %% ══════════════════════════════════════════════════════════ %% %% COOK (INTERACTIVE) PATH %% %% ══════════════════════════════════════════════════════════ %% subgraph CookPath ["Cook (Interactive) Session Path (cli/_cook.py ●)"] direction TB COOK["● cook()<br/>━━━━━━━━━━<br/>User-facing session launcher<br/>Imports _MAX_MCP_OUTPUT_TOKENS_VALUE<br/>from autoskillit.execution ●"] EXTRAS_C["● Assemble cook extras<br/>━━━━━━━━━━<br/>MAX_MCP_OUTPUT_TOKENS = 50000 ← new<br/>passed as env_extras=..."] BIC["build_interactive_cmd()<br/>━━━━━━━━━━<br/>cmd: [claude, --dangerously-skip-permissions,<br/> --plugin-dir, --add-dir, ...]<br/>env: build_claude_env(extras=env_extras)"] end %% ══════════════════════════════════════════════════════════ %% %% SHARED ENV BUILD LAYER (_claude_env.py ●) %% %% ══════════════════════════════════════════════════════════ %% subgraph EnvBuild ["● build_claude_env() (core/_claude_env.py ●)"] direction TB SRC{"base provided?"} USE_BASE["Use provided base<br/>(filtered_base for headless)"] USE_OSENV["Use os.environ<br/>(cook path)"] SCRUB["● Denylist scrub loop<br/>━━━━━━━━━━<br/>For each key in src:<br/> reject if key ∈ IDE_ENV_DENYLIST ← extended<br/> reject if key starts with prefix in<br/> IDE_ENV_PREFIX_DENYLIST"] DENYLIST_NOTE["IDE_ENV_DENYLIST now includes:<br/>━━━━━━━━━━<br/>MAX_MCP_OUTPUT_TOKENS ← new<br/>CLAUDE_CODE_EXIT_AFTER_STOP_DELAY ← new<br/>SCENARIO_STEP_NAME ← new<br/>+ original IDE vars"] ALWAYS["Inject IDE_ENV_ALWAYS_EXTRAS<br/>━━━━━━━━━━<br/>CLAUDE_CODE_AUTO_CONNECT_IDE = 0"] MERGE_EXTRAS["Merge caller extras<br/>━━━━━━━━━━<br/>Including MAX_MCP_OUTPUT_TOKENS = 50000<br/>(always wins — injected after scrub)"] SEAL["Return MappingProxyType<br/>━━━━━━━━━━<br/>Read-only sealed env<br/>prevents post-build mutation"] end %% ══════════════════════════════════════════════════════════ %% %% RE-EXPORT BRIDGE (execution/__init__.py ●) %% %% ══════════════════════════════════════════════════════════ %% REEXPORT["● execution/__init__.py<br/>━━━━━━━━━━<br/>Re-exports _MAX_MCP_OUTPUT_TOKENS_VALUE<br/>from commands.py<br/>(avoids submodule import in _cook.py)"] %% ── FLOW ─────────────────────────────────────────────────── %% ENTRY -->|"headless"| BFH ENTRY -->|"cook / interactive"| COOK BFH --> FILTER FILTER --> PROMPT FILTER --> EXTRAS_H EXTRAS_H --> OPT_DELAY OPT_DELAY -->|"yes"| INJECT_DELAY OPT_DELAY -->|"no"| OPT_STEP INJECT_DELAY --> OPT_STEP OPT_STEP -->|"yes"| INJECT_STEP OPT_STEP -->|"no"| BHC INJECT_STEP --> BHC PROMPT --> BHC COOK --> REEXPORT REEXPORT -->|"_MAX_MCP_OUTPUT_TOKENS_VALUE"| EXTRAS_C COOK --> EXTRAS_C EXTRAS_C --> BIC BHC --> SRC BIC --> SRC SRC -->|"yes (headless)"| USE_BASE SRC -->|"no (cook)"| USE_OSENV USE_BASE --> SCRUB USE_OSENV --> SCRUB DENYLIST_NOTE -.->|"defines"| SCRUB SCRUB --> ALWAYS ALWAYS --> MERGE_EXTRAS MERGE_EXTRAS --> SEAL SEAL --> DONE %% ── CLASS ASSIGNMENTS ────────────────────────────────────── %% class START,DONE terminal; class ENTRY,OPT_DELAY,OPT_STEP,SRC stateNode; class BFH,BHC,BIC,COOK,PROMPT handler; class FILTER,EXTRAS_H,EXTRAS_C,REEXPORT phase; class SCRUB,DENYLIST_NOTE detector; class INJECT_DELAY,INJECT_STEP,ALWAYS,MERGE_EXTRAS,USE_BASE,USE_OSENV,SEAL output;Module Dependency Diagram
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 70, 'curve': 'basis'}}}%% graph TB %% CLASS DEFINITIONS %% classDef cli fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff; classDef stateNode fill:#004d40,stroke:#4db6ac,stroke-width:2px,color:#fff; classDef handler fill:#e65100,stroke:#ffb74d,stroke-width:2px,color:#fff; classDef phase fill:#6a1b9a,stroke:#ba68c8,stroke-width:2px,color:#fff; classDef integration fill:#c62828,stroke:#ef9a9a,stroke-width:2px,color:#fff; classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff; subgraph LayerCLI ["L3 — CLI LAYER"] direction LR COOK["● cli/_cook.py<br/>━━━━━━━━━━<br/>cook(): interactive skill launcher<br/>Imports _MAX_MCP_OUTPUT_TOKENS_VALUE<br/>Passes as env_extras to build_interactive_cmd"] end subgraph LayerExec ["L1 — EXECUTION LAYER"] direction LR EXEC_INIT["● execution/__init__.py<br/>━━━━━━━━━━<br/>Re-exports _MAX_MCP_OUTPUT_TOKENS_VALUE<br/>so cli/ avoids submodule import"] COMMANDS["● execution/commands.py<br/>━━━━━━━━━━<br/>_MAX_MCP_OUTPUT_TOKENS_VALUE = '50000'<br/>_HEADLESS_EXCLUSIVE_VARS (frozenset)<br/>build_full_headless_cmd(): injects token limit<br/>build_interactive_cmd(): caller-supplied extras<br/>build_headless_cmd(): delegates to build_claude_env"] end subgraph LayerCore ["L0 — CORE LAYER"] direction LR CLAUDE_ENV["● core/_claude_env.py<br/>━━━━━━━━━━<br/>IDE_ENV_DENYLIST: strips MAX_MCP_OUTPUT_TOKENS<br/>IDE_ENV_PREFIX_DENYLIST<br/>IDE_ENV_ALWAYS_EXTRAS<br/>build_claude_env(): scrub + seal env"] CORE_INIT["core/__init__.py<br/>━━━━━━━━━━<br/>Re-exports build_claude_env<br/>for execution layer consumers"] end subgraph LayerExt ["EXTERNAL"] direction LR OS_ENVIRON["os.environ<br/>━━━━━━━━━━<br/>Host process environment<br/>(MAX_MCP_OUTPUT_TOKENS stripped<br/>by denylist before child launch)"] CLAUDE_PROC["claude subprocess<br/>━━━━━━━━━━<br/>Receives sealed env<br/>MAX_MCP_OUTPUT_TOKENS=50000<br/>CLAUDE_CODE_AUTO_CONNECT_IDE=0"] end %% VALID DEPENDENCIES (downward) %% COOK -->|"from autoskillit.execution import<br/>_MAX_MCP_OUTPUT_TOKENS_VALUE,<br/>build_interactive_cmd"| EXEC_INIT EXEC_INIT -->|"re-exports from"| COMMANDS COMMANDS -->|"from autoskillit.core import<br/>build_claude_env, ClaudeFlags,<br/>ValidatedAddDir, temp_dir_display_str"| CORE_INIT CORE_INIT -->|"re-exports from"| CLAUDE_ENV %% DATA FLOW %% OS_ENVIRON -->|"base env (filtered:<br/>_HEADLESS_EXCLUSIVE_VARS removed)"| COMMANDS CLAUDE_ENV -->|"MappingProxyType<br/>scrubbed + sealed"| COMMANDS COMMANDS -->|"ClaudeHeadlessCmd / ClaudeInteractiveCmd<br/>(cmd=[], env=sealed)"| COOK COOK -->|"subprocess.run(cmd, env=spec.env)"| CLAUDE_PROC %% DESIGN INVARIANTS %% NOTE1["Design Invariant<br/>━━━━━━━━━━<br/>MAX_MCP_OUTPUT_TOKENS in denylist →<br/>cannot leak from host env into child.<br/>Injected only via explicit extras."] %% CLASS ASSIGNMENTS %% class COOK cli; class EXEC_INIT,COMMANDS handler; class CLAUDE_ENV,CORE_INIT stateNode; class OS_ENVIRON integration; class CLAUDE_PROC output; class NOTE1 phase;Closes #907
Implementation Plan
Plan file:
/home/talon/projects/autoskillit-runs/impl-20260414-065641-276152/.autoskillit/temp/make-plan/inject_max_mcp_output_tokens_plan_2026-04-14_070000.md🤖 Generated with Claude Code via AutoSkillit
Token Usage Summary