Problem
Running engine_preflight (the mcp__atlas-engine__engine_preflight MCP tool / engine-preflight CLI subcommand) silently appends keys to the project's .env file. A "preflight"/"doctor"/"check" step is expected to be read-only — it should inspect state, not mutate user files without warning. Today the only way to stop the .env write is the all-or-nothing --no-configure flag, which also disables the wanted .taskmaster/config.json provider setup. The write should be (a) off by default during preflight, (b) separately opt-in from the rest of provider configuration, and (c) loudly announced whenever it does happen.
Current Behavior
engine_preflight is dispatched via cmd_engine_preflight → run_engine_preflight(configure=...) in prd_taskmaster/batch.py. When configure=True (the default; only flipped off by --no-configure) AND a .taskmaster/ project already exists, run_engine_preflight calls run_configure_providers() (batch.py:85-89).
Inside run_configure_providers (prd_taskmaster/providers.py:151-254), when the research role resolves to the local Perplexity-free proxy (_is_local_perplexity_free(...) true — provider in {openai-compatible, perplexity} plus a 127.0.0.1:8765 / localhost:8765 / perplexity-api-free baseURL), it appends two keys to .env via _ensure_env_entry(Path(".env"), ...):
OPENAI_COMPATIBLE_API_KEY="local-perplexity-api-free" (providers.py:223-224)
PERPLEXITY_API_BASE_URL="<local proxy url>" (providers.py:225-226)
and mirrors both into .env.example if that file exists (providers.py:227-241).
The low-level primitive _ensure_env_entry (prd_taskmaster/lib.py:275-284) opens the file in append mode and writes KEY="value". It is already non-clobbering — guarded by _env_file_has_key (lib.py:268-272), it returns False/no-op if the key already exists (lib.py:277), and it appends a trailing newline safely (lib.py:280-283). So "never overwrite an existing value" is already satisfied.
The genuine defects:
- ON by default — the write fires during a normal
engine_preflight with no opt-in.
- Not separately opt-in — only
--no-configure (cli.py:143) disables it, and that also suppresses the wanted config.json writes.
- Silent / unannounced — the only signal is a
".env:OPENAI_COMPATIBLE_API_KEY" / ".env:PERPLEXITY_API_BASE_URL" string buried in the changed list inside the returned providers_configured dict. The human-readable summary (batch.py:94-115) never mentions that .env was touched. (Note: the docstring at batch.py:75 even claims the call is "Read-only on a bare directory" — but it is not read-only once a project exists.)
The values written are non-secret (a dummy local-perplexity-api-free key and a localhost URL), and the trigger is narrow, but the mutation being on-by-default and invisible is the issue.
Expected Behavior
engine_preflight should be read-only with respect to the user's .env. It should still detect and report that the research role resolves to the local Perplexity-free proxy, and surface the would-be .env change as a pending/planned action in the human-readable summary, naming the exact keys and the command to run to apply it. The actual .env write should only happen via an explicit, opt-in step (a dedicated flag or the standalone configure-providers subcommand), and when it does run it must print an explicit "Wrote .env: KEY1, KEY2" line. The non-clobbering behavior (_ensure_env_entry skips existing keys) must be preserved.
Files to Touch
/home/anombyte/.claude/skills/prd-taskmaster/prd_taskmaster/providers.py — run_configure_providers (L151-254). Add an apply_env: bool = True parameter. Guard the .env/.env.example write block (L222-241) on apply_env; when apply_env is False, push a ".env:PENDING:OPENAI_COMPATIBLE_API_KEY" / ".env:PENDING:PERPLEXITY_API_BASE_URL" marker into changed instead of writing. PRIMARY change site.
/home/anombyte/.claude/skills/prd-taskmaster/prd_taskmaster/batch.py — run_engine_preflight (L72-126). Call run_configure_providers(apply_env=False) so preflight is read-only on .env. In the summary builder (L94-115), detect .env:PENDING:* markers in providers_configured["changed"] and append a human line naming the keys + the apply command. Also add an env_writes / env_pending field to the returned dict. Thread any new opt-in flag from cmd_engine_preflight (L129-133).
/home/anombyte/.claude/skills/prd-taskmaster/prd_taskmaster/cli.py — engine-preflight subparser (L142-143). If using the dedicated-flag approach, register --write-env (or --no-write-env) next to --no-configure and thread args.write_env through cmd_engine_preflight. The standalone configure-providers subcommand (DISPATCH ~L302-309, cmd_configure_providers at providers.py:257) keeps apply_env=True so it performs the real write.
/home/anombyte/.claude/skills/prd-taskmaster/prd_taskmaster/lib.py — _ensure_env_entry (L275-284), _env_file_has_key (L268-272), _is_local_perplexity_free (L320-334). No change required unless you choose to add logging at this chokepoint; already non-clobbering. Reference only.
/home/anombyte/Shade_Gen/Projects/prd-taskmaster-plugin/mcp-server/pipeline.py — preflight (L179-234). NOT the code to change; the stale atlas-go plugin's read-only preflight, included only to disambiguate (it never touches .env). Flag if the team wants to retire this duplicate.
Researched Fix Approaches
1. [Recommended] — Make engine_preflight read-only; move the .env write into the explicit configure step (with announce) (confidence: 86%)
- Library/Config: stdlib
argparse (cli.py); no new dependency. Verified task-master-ai convention: it reads .env only and never auto-writes it.
- Pattern: plan/preview vs apply separation (Terraform
plan→apply, Ansible --check, clig.dev). A preflight/doctor/check command must be read-only; surface the .env change as a planned action, write it only via the existing standalone configure-providers subcommand.
- Why: Restores parity with the tool being wrapped —
task-master-ai only reads .env, so the wrapper's silent append is behavior the upstream tool does not have. The escape hatch already exists (configure-providers is a separate subcommand), giving the write a natural opt-in home preflight need not trigger.
- Risk: Behavioral change — callers/MCP hosts that relied on preflight auto-writing
.env (so the local-perplexity-free path "just works" after one preflight) now need an explicit apply step. Mitigate by making the preflight summary print the exact command to run. Note: the config.json write at providers.py:244 is also a mutation during preflight, but it is lower-stakes (tool-owned dir, not user .env) and out of scope here.
- Implementation hint:
# providers.py — run_configure_providers signature + guard
def run_configure_providers(economy: str | None = None, apply_env: bool = True) -> dict:
...
if _is_local_perplexity_free(models.get("research", {})):
if apply_env:
if _ensure_env_entry(Path(".env"), "OPENAI_COMPATIBLE_API_KEY", "local-perplexity-api-free"):
changed.append(".env:OPENAI_COMPATIBLE_API_KEY")
if _ensure_env_entry(Path(".env"), "PERPLEXITY_API_BASE_URL", local_proxy_url):
changed.append(".env:PERPLEXITY_API_BASE_URL")
# ... .env.example mirror unchanged ...
else:
changed.append(".env:PENDING:OPENAI_COMPATIBLE_API_KEY")
changed.append(".env:PENDING:PERPLEXITY_API_BASE_URL")
# batch.py — run_engine_preflight
providers_configured = run_configure_providers(apply_env=False)
...
pending = [m for m in (providers_configured or {}).get("changed", []) if m.startswith(".env:PENDING:")]
if pending:
keys = ", ".join(m.split(":", 2)[2] for m in pending)
summary.append(f"Pending .env: {keys} (run `configure-providers` to write)")
# cmd_configure_providers keeps apply_env=True (default) so the standalone command really writes.
2. [Alternative] — Dedicated, independent --write-env / --no-write-env opt-in flag (default OFF), always announce (confidence: 80%)
- Library/Config: stdlib
argparse only. Flag naming verified against clig.dev (--dry-run/-n, --no-<x> negation, explicit --write/--apply for mutations).
- Pattern: explicit opt-in side-effect flag, orthogonal to the existing
--no-configure. Register --write-env (action="store_true", default False) on the engine-preflight subparser; thread args.write_env → cmd_engine_preflight → run_engine_preflight(write_env=...) → run_configure_providers(apply_env=write_env). Print "Wrote .env: KEY1, KEY2" whenever a write occurs.
- Why: Directly fixes all three named defects: ON-by-default → OFF, all-or-nothing → its own flag orthogonal to
--no-configure (you can still get config.json without the .env append), silent → always announced. Lowest blast radius — config.json behavior untouched.
- Risk: A purist reading of "preflight must be read-only" dislikes that preflight technically CAN still mutate (when
--write-env is passed). Requires the separate MCP host (which shells script.py engine-preflight) to learn the new flag for the write to ever fire from MCP — confirm the host can thread flags, or the write becomes unreachable from MCP.
- Implementation hint:
# cli.py
p.add_argument("--write-env", action="store_true",
help="Opt in to appending OPENAI_COMPATIBLE_API_KEY / PERPLEXITY_API_BASE_URL to .env (local Perplexity-free proxy only)")
# cmd_engine_preflight
run_engine_preflight(configure=not args.no_configure, write_env=getattr(args, "write_env", False))
# batch.py summary, after a real write:
summary.append(f"Wrote .env: {', '.join(written_keys)}")
3. [Fallback] — Announce-only: keep auto-write but surface every .env mutation in the summary + a structured env_writes field (confidence: 58%)
- Library/Config: stdlib only; relies on the existing
changed markers at providers.py:224/226/234/241.
- Pattern: non-destructive-by-virtue-of-already-idempotent + loud logging. No default change, no new flag —
batch.py reads providers_configured["changed"], extracts .env:* markers, adds an explicit summary line and an env_writes top-level field.
- Why: Smallest, lowest-risk diff resolving the single most-cited defect (silent/unannounced). The write is already non-clobbering and idempotent and the values are non-secret, so "never clobber" already holds — only the announcement is missing.
- Risk: Does NOT satisfy the opt-in requirement; the write stays ON by default during a preflight, which violates the read-only convention and upstream parity. Likely rejected if the intent is "preflight must not write user files at all." Treat as a stopgap or complement to Approach 1/2.
- Implementation hint:
# batch.py — run_engine_preflight, after computing providers_configured
env_writes = [m for m in (providers_configured or {}).get("changed", []) if m.startswith(".env")]
if env_writes:
summary.append("Modified " + ", ".join(env_writes))
# add 'env_writes': env_writes to the returned dict
Reference
The wrapped tool task-master-ai (claude-task-master by eyaltoledano) is the decisive reference: it reads API keys EXCLUSIVELY from .env (CLI) or the MCP env block and — verified against its docs and .env.example on main — NEVER auto-writes the user's .env. Its setup/models flow creates only .taskmaster/config.json; keys/baseURLs are documented for the user to add themselves. So Atlas's engine_preflight is MORE intrusive than the tool it wraps. Aligning the wrapper (preflight = read-only; .env writes only via an explicit, announced step) restores parity. Broader CLI convention (clig.dev, Terraform plan→apply, Ansible --check, AWS --dry-run): doctor/preflight/check commands are read-only, mutations need a distinct apply/--write step or confirmation, and every file write must be announced. NOTE: upstream task-master-ai does NOT document OPENAI_COMPATIBLE_API_KEY (the key the Atlas wrapper writes) — verify what task-master's openai-compatible provider actually reads before relying on that key name.
Sources: task-master-ai .env.example and configuration-advanced docs; clig.dev; Terraform plan/apply; "In Praise of Dry Run" (henrikwarne.com).
Acceptance Criteria
Complexity: S
Trust Level: HINT (not specification)
The researched approaches above are starting points. Before implementing:
- Verify the library/config exists as stated (e.g.
task-master models, read .taskmaster/config.json; confirm whether the MCP host that exposes mcp__atlas-engine__engine_preflight can pass CLI flags before choosing Approach 2).
- Check that imports/keys match reality (especially whether task-master's openai-compatible provider reads
OPENAI_COMPATIBLE_API_KEY).
- Try the recommended approach — if it works in 1-2 attempts, use it.
- If it fails, do NOT keep retrying — research why, explore alternatives.
- The acceptance criteria are the real spec, not the approach.
Problem
Running
engine_preflight(themcp__atlas-engine__engine_preflightMCP tool /engine-preflightCLI subcommand) silently appends keys to the project's.envfile. A "preflight"/"doctor"/"check" step is expected to be read-only — it should inspect state, not mutate user files without warning. Today the only way to stop the.envwrite is the all-or-nothing--no-configureflag, which also disables the wanted.taskmaster/config.jsonprovider setup. The write should be (a) off by default during preflight, (b) separately opt-in from the rest of provider configuration, and (c) loudly announced whenever it does happen.Current Behavior
engine_preflightis dispatched viacmd_engine_preflight→run_engine_preflight(configure=...)inprd_taskmaster/batch.py. Whenconfigure=True(the default; only flipped off by--no-configure) AND a.taskmaster/project already exists,run_engine_preflightcallsrun_configure_providers()(batch.py:85-89).Inside
run_configure_providers(prd_taskmaster/providers.py:151-254), when the research role resolves to the local Perplexity-free proxy (_is_local_perplexity_free(...)true — provider in{openai-compatible, perplexity}plus a127.0.0.1:8765/localhost:8765/perplexity-api-freebaseURL), it appends two keys to.envvia_ensure_env_entry(Path(".env"), ...):OPENAI_COMPATIBLE_API_KEY="local-perplexity-api-free"(providers.py:223-224)PERPLEXITY_API_BASE_URL="<local proxy url>"(providers.py:225-226)and mirrors both into
.env.exampleif that file exists (providers.py:227-241).The low-level primitive
_ensure_env_entry(prd_taskmaster/lib.py:275-284) opens the file in append mode and writesKEY="value". It is already non-clobbering — guarded by_env_file_has_key(lib.py:268-272), it returnsFalse/no-op if the key already exists (lib.py:277), and it appends a trailing newline safely (lib.py:280-283). So "never overwrite an existing value" is already satisfied.The genuine defects:
engine_preflightwith no opt-in.--no-configure(cli.py:143) disables it, and that also suppresses the wantedconfig.jsonwrites.".env:OPENAI_COMPATIBLE_API_KEY"/".env:PERPLEXITY_API_BASE_URL"string buried in thechangedlist inside the returnedproviders_configureddict. The human-readablesummary(batch.py:94-115) never mentions that.envwas touched. (Note: the docstring atbatch.py:75even claims the call is "Read-only on a bare directory" — but it is not read-only once a project exists.)The values written are non-secret (a dummy
local-perplexity-api-freekey and a localhost URL), and the trigger is narrow, but the mutation being on-by-default and invisible is the issue.Expected Behavior
engine_preflightshould be read-only with respect to the user's.env. It should still detect and report that the research role resolves to the local Perplexity-free proxy, and surface the would-be.envchange as a pending/planned action in the human-readable summary, naming the exact keys and the command to run to apply it. The actual.envwrite should only happen via an explicit, opt-in step (a dedicated flag or the standaloneconfigure-providerssubcommand), and when it does run it must print an explicit "Wrote .env: KEY1, KEY2" line. The non-clobbering behavior (_ensure_env_entryskips existing keys) must be preserved.Files to Touch
/home/anombyte/.claude/skills/prd-taskmaster/prd_taskmaster/providers.py—run_configure_providers(L151-254). Add anapply_env: bool = Trueparameter. Guard the.env/.env.examplewrite block (L222-241) onapply_env; whenapply_envis False, push a".env:PENDING:OPENAI_COMPATIBLE_API_KEY"/".env:PENDING:PERPLEXITY_API_BASE_URL"marker intochangedinstead of writing. PRIMARY change site./home/anombyte/.claude/skills/prd-taskmaster/prd_taskmaster/batch.py—run_engine_preflight(L72-126). Callrun_configure_providers(apply_env=False)so preflight is read-only on.env. In the summary builder (L94-115), detect.env:PENDING:*markers inproviders_configured["changed"]and append a human line naming the keys + the apply command. Also add anenv_writes/env_pendingfield to the returned dict. Thread any new opt-in flag fromcmd_engine_preflight(L129-133)./home/anombyte/.claude/skills/prd-taskmaster/prd_taskmaster/cli.py— engine-preflight subparser (L142-143). If using the dedicated-flag approach, register--write-env(or--no-write-env) next to--no-configureand threadargs.write_envthroughcmd_engine_preflight. The standaloneconfigure-providerssubcommand (DISPATCH ~L302-309,cmd_configure_providersatproviders.py:257) keepsapply_env=Trueso it performs the real write./home/anombyte/.claude/skills/prd-taskmaster/prd_taskmaster/lib.py—_ensure_env_entry(L275-284),_env_file_has_key(L268-272),_is_local_perplexity_free(L320-334). No change required unless you choose to add logging at this chokepoint; already non-clobbering. Reference only./home/anombyte/Shade_Gen/Projects/prd-taskmaster-plugin/mcp-server/pipeline.py—preflight(L179-234). NOT the code to change; the staleatlas-goplugin's read-only preflight, included only to disambiguate (it never touches.env). Flag if the team wants to retire this duplicate.Researched Fix Approaches
1. [Recommended] — Make engine_preflight read-only; move the
.envwrite into the explicit configure step (with announce) (confidence: 86%)argparse(cli.py); no new dependency. Verifiedtask-master-aiconvention: it reads.envonly and never auto-writes it.plan→apply, Ansible--check, clig.dev). A preflight/doctor/check command must be read-only; surface the.envchange as a planned action, write it only via the existing standaloneconfigure-providerssubcommand.task-master-aionly reads.env, so the wrapper's silent append is behavior the upstream tool does not have. The escape hatch already exists (configure-providersis a separate subcommand), giving the write a natural opt-in home preflight need not trigger..env(so the local-perplexity-free path "just works" after one preflight) now need an explicit apply step. Mitigate by making the preflight summary print the exact command to run. Note: theconfig.jsonwrite atproviders.py:244is also a mutation during preflight, but it is lower-stakes (tool-owned dir, not user.env) and out of scope here.2. [Alternative] — Dedicated, independent
--write-env/--no-write-envopt-in flag (default OFF), always announce (confidence: 80%)argparseonly. Flag naming verified against clig.dev (--dry-run/-n,--no-<x>negation, explicit--write/--applyfor mutations).--no-configure. Register--write-env(action="store_true", defaultFalse) on the engine-preflight subparser; threadargs.write_env→cmd_engine_preflight→run_engine_preflight(write_env=...)→run_configure_providers(apply_env=write_env). Print "Wrote .env: KEY1, KEY2" whenever a write occurs.--no-configure(you can still getconfig.jsonwithout the.envappend), silent → always announced. Lowest blast radius —config.jsonbehavior untouched.--write-envis passed). Requires the separate MCP host (which shellsscript.py engine-preflight) to learn the new flag for the write to ever fire from MCP — confirm the host can thread flags, or the write becomes unreachable from MCP.3. [Fallback] — Announce-only: keep auto-write but surface every
.envmutation in the summary + a structuredenv_writesfield (confidence: 58%)changedmarkers atproviders.py:224/226/234/241.batch.pyreadsproviders_configured["changed"], extracts.env:*markers, adds an explicit summary line and anenv_writestop-level field.Reference
The wrapped tool
task-master-ai(claude-task-master by eyaltoledano) is the decisive reference: it reads API keys EXCLUSIVELY from.env(CLI) or the MCP env block and — verified against its docs and.env.exampleonmain— NEVER auto-writes the user's.env. Its setup/models flow creates only.taskmaster/config.json; keys/baseURLs are documented for the user to add themselves. So Atlas'sengine_preflightis MORE intrusive than the tool it wraps. Aligning the wrapper (preflight = read-only;.envwrites only via an explicit, announced step) restores parity. Broader CLI convention (clig.dev, Terraformplan→apply, Ansible--check, AWS--dry-run): doctor/preflight/check commands are read-only, mutations need a distinct apply/--writestep or confirmation, and every file write must be announced. NOTE: upstreamtask-master-aidoes NOT documentOPENAI_COMPATIBLE_API_KEY(the key the Atlas wrapper writes) — verify what task-master's openai-compatible provider actually reads before relying on that key name.Sources:
task-master-ai.env.exampleand configuration-advanced docs; clig.dev; Terraform plan/apply; "In Praise of Dry Run" (henrikwarne.com).Acceptance Criteria
engine_preflight(default flags) on a project where.taskmaster/config.jsonresolves research to the local Perplexity-free proxy does NOT appendOPENAI_COMPATIBLE_API_KEYorPERPLEXITY_API_BASE_URLto.env— verify.envbyte-for-byte unchanged before/after (git diff --exit-code .env).summary(and/or a structuredenv_pending/env_writesfield in the returned dict) explicitly names the two keys and tells the operator how to apply them (e.g. referencesconfigure-providersor--write-env).configure-providers, ORengine-preflight --write-env, per chosen approach) DOES write both keys to.envand prints an explicit "Wrote .env: ..." style line naming the keys._ensure_env_entrystill returnsFalsefor existing keys,.envis unchanged, and no spurious "Wrote .env" line is emitted.OPENAI_COMPATIBLE_API_KEYorPERPLEXITY_API_BASE_URLwith a different value in.envis NEVER overwritten by either path (non-clobber preserved).engine_preflight --no-configurestill suppresses ALL provider configuration including theconfig.jsonwrites (existing behavior unchanged), and the new.envopt-in is orthogonal to it (you can getconfig.jsonconfiguration without the.envwrite).apply_env=False/preview,.taskmaster/config.jsonis still written/updated as before — only the.envmutation is deferred.parse_prdsucceeds and writes tasks to a fresh tag when invoked from inside a Claude Code / MCP session after running the read-onlyengine_preflightfollowed by the explicit configure step (end-to-end the research path still works once.envis applied).batch.py:75("Read-only on a bare directory") is updated to accurately reflect that preflight no longer mutates.env.Complexity: S
Trust Level: HINT (not specification)
The researched approaches above are starting points. Before implementing:
task-master models, read.taskmaster/config.json; confirm whether the MCP host that exposesmcp__atlas-engine__engine_preflightcan pass CLI flags before choosing Approach 2).OPENAI_COMPATIBLE_API_KEY).