[{"data":1,"prerenderedAt":2985},["ShallowReactive",2],{"page-\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Freading-secrets-from-env-and-files\u002F":3,"content-directory":2438},{"id":4,"title":5,"body":6,"date":2423,"description":2424,"difficulty":2425,"draft":2426,"extension":2427,"meta":2428,"navigation":163,"path":2429,"seo":2430,"stem":2431,"tags":2432,"updated":2423,"__hash__":2437},"content\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Freading-secrets-from-env-and-files\u002Findex.md","Reading Secrets from Env Vars and Files in CLIs",{"type":7,"value":8,"toc":2403},"minimark",[9,32,37,54,62,65,69,72,76,79,82,89,96,115,118,122,1043,1046,1449,1454,1473,1482,1492,1515,1533,1537,1598,1602,1612,2274,2293,2297,2303,2307,2315,2322,2326,2336,2340,2353,2357,2364,2368,2399],[10,11,12,13,17,18,21,22,25,26,31],"p",{},"A person at a terminal can log in once and let the keychain remember. Automation cannot: a GitHub Actions job, a Kubernetes CronJob or a Docker container has to receive its credentials from outside, from whatever secret manager the platform uses. Those platforms deliver secrets in two standard shapes — as environment variables and as files mounted into the filesystem — and a CLI that supports both, correctly, drops into any pipeline without wrappers. This guide implements a small secret-reading helper that handles ",[14,15,16],"code",{},"MYTOOL_TOKEN",", the ",[14,19,20],{},"MYTOOL_TOKEN_FILE"," convention and ",[14,23,24],{},"--token-stdin",", checks file permissions, strips the trailing newline that breaks so many tokens, and keeps the secrets out of child processes. It is part of the ",[27,28,30],"a",{"href":29},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002F","secrets and credentials topic",".",[33,34,36],"h2",{"id":35},"prerequisites","Prerequisites",[38,39,40,44,47],"ul",{},[41,42,43],"li",{},"Python 3.10+ and Typer.",[41,45,46],{},"A CLI that needs one or more credentials — API tokens, a database password, a signing key.",[41,48,49,50,31],{},"For the interactive side of the same story, see ",[27,51,53],{"href":52},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring\u002F","storing tokens with keyring",[33,55,57,58,61],{"id":56},"why-not-just-a-token-flag","Why not just a ",[14,59,60],{},"--token"," flag?",[10,63,64],{},"Because arguments are the most public place a secret can be. On Linux and macOS, every user on the machine can list every process's full command line for as long as it runs; shells save commands to history files; CI systems echo the commands they run into logs that many people can read.",[66,67],"inline-diagram",{"name":68},"sec-argv-leak-terminal",[10,70,71],{},"Environment variables and files avoid all three. A process's environment is readable only by the same user and root, and it is not written to history. A file's visibility is governed by its permissions, and it is not inherited by child processes at all.",[33,73,75],{"id":74},"environment-variable-or-file","Environment variable or file?",[10,77,78],{},"Both are worth supporting, because different platforms favour different ones:",[66,80],{"name":81},"sec-env-vs-file",[10,83,84,85,88],{},"Environment variables are universal and convenient: every CI system can set them from its secret store, and they are trivial to use locally (",[14,86,87],{},"MYTOOL_TOKEN=... mytool deploy","). Their weaknesses are inheritance — every subprocess gets them — and the fact that they are fixed for the lifetime of the process.",[10,90,91,92,95],{},"Files are what container orchestrators prefer. Docker secrets appear under ",[14,93,94],{},"\u002Frun\u002Fsecrets\u002F\u003Cname>","; Kubernetes can mount secrets as files anywhere; Vault Agent and similar sidecars write rendered secrets to disk and refresh them on rotation. A long-running process that re-reads the file picks up a rotated credential without a restart.",[10,97,98,99,102,103,106,107,110,111,114],{},"The convention that ties them together comes from official Docker images such as ",[14,100,101],{},"postgres"," and ",[14,104,105],{},"mysql",": for any secret variable ",[14,108,109],{},"NAME",", also accept ",[14,112,113],{},"NAME_FILE"," containing a path to read the value from. Operators recognise it immediately.",[66,116],{"name":117},"sec-file-convention",[33,119,121],{"id":120},"the-recipe","The recipe",[123,124,129],"pre",{"className":125,"code":126,"language":127,"meta":128,"style":128},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Fsecrets.py\nfrom __future__ import annotations\n\nimport os\nimport stat\nimport sys\nimport warnings\nfrom pathlib import Path\n\n\nclass Secret:\n    __slots__ = (\"_v\",)\n\n    def __init__(self, value: str) -> None:\n        self._v = value\n\n    def reveal(self) -> str:\n        return self._v\n\n    def __repr__(self) -> str:\n        return \"Secret('********')\"\n\n    __str__ = __repr__\n\n\nclass MissingSecret(Exception):\n    pass\n\n\ndef _read_file(path: Path) -> str:\n    try:\n        info = path.stat()\n    except FileNotFoundError:\n        raise MissingSecret(f\"secret file {path} does not exist\") from None\n    if os.name == \"posix\" and info.st_mode & (stat.S_IRWXG | stat.S_IRWXO):\n        warnings.warn(f\"{path} is readable by other users; chmod 600 it\", stacklevel=3)\n    value = path.read_text(encoding=\"utf-8\")\n    return value.rstrip(\"\\r\\n\")                # editors and `echo` add a newline\n\n\ndef read_secret(name: str, *, stdin: bool = False, required: bool = True) -> Secret | None:\n    \"\"\"Resolve secret NAME from stdin, NAME_FILE or NAME, in that order.\"\"\"\n    if stdin:\n        value = sys.stdin.readline().rstrip(\"\\r\\n\")\n        if not value:\n            raise MissingSecret(f\"expected {name} on stdin, got nothing\")\n        return Secret(value)\n    if file_path := os.environ.get(f\"{name}_FILE\"):\n        if name in os.environ:\n            warnings.warn(f\"both {name} and {name}_FILE are set; using {name}_FILE\", stacklevel=2)\n        return Secret(_read_file(Path(file_path)))\n    if value := os.environ.get(name):\n        return Secret(value.strip())\n    if required:\n        raise MissingSecret(f\"set {name} or {name}_FILE (or pass it on stdin)\")\n    return None\n\n\ndef child_env(*, keep: tuple[str, ...] = (), drop_prefixes: tuple[str, ...] = (\"MYTOOL_\",)) -> dict[str, str]:\n    \"\"\"A copy of the environment without our secrets, for subprocesses.\"\"\"\n    return {\n        k: v for k, v in os.environ.items()\n        if k in keep or not k.startswith(drop_prefixes)\n    }\n","python","",[14,130,131,140,158,165,174,182,190,198,211,216,221,234,253,258,281,296,301,316,328,333,347,355,360,371,376,381,398,404,409,414,430,438,449,460,495,536,571,592,614,619,624,674,680,688,707,719,744,752,781,795,842,850,863,871,879,911,918,923,928,985,991,999,1016,1037],{"__ignoreMap":128},[132,133,136],"span",{"class":134,"line":135},"line",1,[132,137,139],{"class":138},"sJ8bj","# src\u002Fmytool\u002Fsecrets.py\n",[132,141,143,147,151,154],{"class":134,"line":142},2,[132,144,146],{"class":145},"szBVR","from",[132,148,150],{"class":149},"sj4cs"," __future__",[132,152,153],{"class":145}," import",[132,155,157],{"class":156},"sVt8B"," annotations\n",[132,159,161],{"class":134,"line":160},3,[132,162,164],{"emptyLinePlaceholder":163},true,"\n",[132,166,168,171],{"class":134,"line":167},4,[132,169,170],{"class":145},"import",[132,172,173],{"class":156}," os\n",[132,175,177,179],{"class":134,"line":176},5,[132,178,170],{"class":145},[132,180,181],{"class":156}," stat\n",[132,183,185,187],{"class":134,"line":184},6,[132,186,170],{"class":145},[132,188,189],{"class":156}," sys\n",[132,191,193,195],{"class":134,"line":192},7,[132,194,170],{"class":145},[132,196,197],{"class":156}," warnings\n",[132,199,201,203,206,208],{"class":134,"line":200},8,[132,202,146],{"class":145},[132,204,205],{"class":156}," pathlib ",[132,207,170],{"class":145},[132,209,210],{"class":156}," Path\n",[132,212,214],{"class":134,"line":213},9,[132,215,164],{"emptyLinePlaceholder":163},[132,217,219],{"class":134,"line":218},10,[132,220,164],{"emptyLinePlaceholder":163},[132,222,224,227,231],{"class":134,"line":223},11,[132,225,226],{"class":145},"class",[132,228,230],{"class":229},"sScJk"," Secret",[132,232,233],{"class":156},":\n",[132,235,237,240,243,246,250],{"class":134,"line":236},12,[132,238,239],{"class":149},"    __slots__",[132,241,242],{"class":145}," =",[132,244,245],{"class":156}," (",[132,247,249],{"class":248},"sZZnC","\"_v\"",[132,251,252],{"class":156},",)\n",[132,254,256],{"class":134,"line":255},13,[132,257,164],{"emptyLinePlaceholder":163},[132,259,261,264,267,270,273,276,279],{"class":134,"line":260},14,[132,262,263],{"class":145},"    def",[132,265,266],{"class":149}," __init__",[132,268,269],{"class":156},"(self, value: ",[132,271,272],{"class":149},"str",[132,274,275],{"class":156},") -> ",[132,277,278],{"class":149},"None",[132,280,233],{"class":156},[132,282,284,287,290,293],{"class":134,"line":283},15,[132,285,286],{"class":149},"        self",[132,288,289],{"class":156},"._v ",[132,291,292],{"class":145},"=",[132,294,295],{"class":156}," value\n",[132,297,299],{"class":134,"line":298},16,[132,300,164],{"emptyLinePlaceholder":163},[132,302,304,306,309,312,314],{"class":134,"line":303},17,[132,305,263],{"class":145},[132,307,308],{"class":229}," reveal",[132,310,311],{"class":156},"(self) -> ",[132,313,272],{"class":149},[132,315,233],{"class":156},[132,317,319,322,325],{"class":134,"line":318},18,[132,320,321],{"class":145},"        return",[132,323,324],{"class":149}," self",[132,326,327],{"class":156},"._v\n",[132,329,331],{"class":134,"line":330},19,[132,332,164],{"emptyLinePlaceholder":163},[132,334,336,338,341,343,345],{"class":134,"line":335},20,[132,337,263],{"class":145},[132,339,340],{"class":149}," __repr__",[132,342,311],{"class":156},[132,344,272],{"class":149},[132,346,233],{"class":156},[132,348,350,352],{"class":134,"line":349},21,[132,351,321],{"class":145},[132,353,354],{"class":248}," \"Secret('********')\"\n",[132,356,358],{"class":134,"line":357},22,[132,359,164],{"emptyLinePlaceholder":163},[132,361,363,366,368],{"class":134,"line":362},23,[132,364,365],{"class":149},"    __str__",[132,367,242],{"class":145},[132,369,370],{"class":149}," __repr__\n",[132,372,374],{"class":134,"line":373},24,[132,375,164],{"emptyLinePlaceholder":163},[132,377,379],{"class":134,"line":378},25,[132,380,164],{"emptyLinePlaceholder":163},[132,382,384,386,389,392,395],{"class":134,"line":383},26,[132,385,226],{"class":145},[132,387,388],{"class":229}," MissingSecret",[132,390,391],{"class":156},"(",[132,393,394],{"class":149},"Exception",[132,396,397],{"class":156},"):\n",[132,399,401],{"class":134,"line":400},27,[132,402,403],{"class":145},"    pass\n",[132,405,407],{"class":134,"line":406},28,[132,408,164],{"emptyLinePlaceholder":163},[132,410,412],{"class":134,"line":411},29,[132,413,164],{"emptyLinePlaceholder":163},[132,415,417,420,423,426,428],{"class":134,"line":416},30,[132,418,419],{"class":145},"def",[132,421,422],{"class":229}," _read_file",[132,424,425],{"class":156},"(path: Path) -> ",[132,427,272],{"class":149},[132,429,233],{"class":156},[132,431,433,436],{"class":134,"line":432},31,[132,434,435],{"class":145},"    try",[132,437,233],{"class":156},[132,439,441,444,446],{"class":134,"line":440},32,[132,442,443],{"class":156},"        info ",[132,445,292],{"class":145},[132,447,448],{"class":156}," path.stat()\n",[132,450,452,455,458],{"class":134,"line":451},33,[132,453,454],{"class":145},"    except",[132,456,457],{"class":149}," FileNotFoundError",[132,459,233],{"class":156},[132,461,463,466,469,472,475,478,481,484,487,490,492],{"class":134,"line":462},34,[132,464,465],{"class":145},"        raise",[132,467,468],{"class":156}," MissingSecret(",[132,470,471],{"class":145},"f",[132,473,474],{"class":248},"\"secret file ",[132,476,477],{"class":149},"{",[132,479,480],{"class":156},"path",[132,482,483],{"class":149},"}",[132,485,486],{"class":248}," does not exist\"",[132,488,489],{"class":156},") ",[132,491,146],{"class":145},[132,493,494],{"class":149}," None\n",[132,496,498,501,504,507,510,513,516,519,522,525,528,531,534],{"class":134,"line":497},35,[132,499,500],{"class":145},"    if",[132,502,503],{"class":156}," os.name ",[132,505,506],{"class":145},"==",[132,508,509],{"class":248}," \"posix\"",[132,511,512],{"class":145}," and",[132,514,515],{"class":156}," info.st_mode ",[132,517,518],{"class":145},"&",[132,520,521],{"class":156}," (stat.",[132,523,524],{"class":149},"S_IRWXG",[132,526,527],{"class":145}," |",[132,529,530],{"class":156}," stat.",[132,532,533],{"class":149},"S_IRWXO",[132,535,397],{"class":156},[132,537,539,542,544,547,549,551,553,556,559,563,565,568],{"class":134,"line":538},36,[132,540,541],{"class":156},"        warnings.warn(",[132,543,471],{"class":145},[132,545,546],{"class":248},"\"",[132,548,477],{"class":149},[132,550,480],{"class":156},[132,552,483],{"class":149},[132,554,555],{"class":248}," is readable by other users; chmod 600 it\"",[132,557,558],{"class":156},", ",[132,560,562],{"class":561},"s4XuR","stacklevel",[132,564,292],{"class":145},[132,566,567],{"class":149},"3",[132,569,570],{"class":156},")\n",[132,572,574,577,579,582,585,587,590],{"class":134,"line":573},37,[132,575,576],{"class":156},"    value ",[132,578,292],{"class":145},[132,580,581],{"class":156}," path.read_text(",[132,583,584],{"class":561},"encoding",[132,586,292],{"class":145},[132,588,589],{"class":248},"\"utf-8\"",[132,591,570],{"class":156},[132,593,595,598,601,603,606,608,611],{"class":134,"line":594},38,[132,596,597],{"class":145},"    return",[132,599,600],{"class":156}," value.rstrip(",[132,602,546],{"class":248},[132,604,605],{"class":149},"\\r\\n",[132,607,546],{"class":248},[132,609,610],{"class":156},")                ",[132,612,613],{"class":138},"# editors and `echo` add a newline\n",[132,615,617],{"class":134,"line":616},39,[132,618,164],{"emptyLinePlaceholder":163},[132,620,622],{"class":134,"line":621},40,[132,623,164],{"emptyLinePlaceholder":163},[132,625,627,629,632,635,637,639,642,645,648,650,653,656,658,660,663,666,669,672],{"class":134,"line":626},41,[132,628,419],{"class":145},[132,630,631],{"class":229}," read_secret",[132,633,634],{"class":156},"(name: ",[132,636,272],{"class":149},[132,638,558],{"class":156},[132,640,641],{"class":145},"*",[132,643,644],{"class":156},", stdin: ",[132,646,647],{"class":149},"bool",[132,649,242],{"class":145},[132,651,652],{"class":149}," False",[132,654,655],{"class":156},", required: ",[132,657,647],{"class":149},[132,659,242],{"class":145},[132,661,662],{"class":149}," True",[132,664,665],{"class":156},") -> Secret ",[132,667,668],{"class":145},"|",[132,670,671],{"class":149}," None",[132,673,233],{"class":156},[132,675,677],{"class":134,"line":676},42,[132,678,679],{"class":248},"    \"\"\"Resolve secret NAME from stdin, NAME_FILE or NAME, in that order.\"\"\"\n",[132,681,683,685],{"class":134,"line":682},43,[132,684,500],{"class":145},[132,686,687],{"class":156}," stdin:\n",[132,689,691,694,696,699,701,703,705],{"class":134,"line":690},44,[132,692,693],{"class":156},"        value ",[132,695,292],{"class":145},[132,697,698],{"class":156}," sys.stdin.readline().rstrip(",[132,700,546],{"class":248},[132,702,605],{"class":149},[132,704,546],{"class":248},[132,706,570],{"class":156},[132,708,710,713,716],{"class":134,"line":709},45,[132,711,712],{"class":145},"        if",[132,714,715],{"class":145}," not",[132,717,718],{"class":156}," value:\n",[132,720,722,725,727,729,732,734,737,739,742],{"class":134,"line":721},46,[132,723,724],{"class":145},"            raise",[132,726,468],{"class":156},[132,728,471],{"class":145},[132,730,731],{"class":248},"\"expected ",[132,733,477],{"class":149},[132,735,736],{"class":156},"name",[132,738,483],{"class":149},[132,740,741],{"class":248}," on stdin, got nothing\"",[132,743,570],{"class":156},[132,745,747,749],{"class":134,"line":746},47,[132,748,321],{"class":145},[132,750,751],{"class":156}," Secret(value)\n",[132,753,755,757,760,763,766,768,770,772,774,776,779],{"class":134,"line":754},48,[132,756,500],{"class":145},[132,758,759],{"class":156}," file_path ",[132,761,762],{"class":145},":=",[132,764,765],{"class":156}," os.environ.get(",[132,767,471],{"class":145},[132,769,546],{"class":248},[132,771,477],{"class":149},[132,773,736],{"class":156},[132,775,483],{"class":149},[132,777,778],{"class":248},"_FILE\"",[132,780,397],{"class":156},[132,782,784,786,789,792],{"class":134,"line":783},49,[132,785,712],{"class":145},[132,787,788],{"class":156}," name ",[132,790,791],{"class":145},"in",[132,793,794],{"class":156}," os.environ:\n",[132,796,798,801,803,806,808,810,812,814,816,818,820,823,825,827,829,831,833,835,837,840],{"class":134,"line":797},50,[132,799,800],{"class":156},"            warnings.warn(",[132,802,471],{"class":145},[132,804,805],{"class":248},"\"both ",[132,807,477],{"class":149},[132,809,736],{"class":156},[132,811,483],{"class":149},[132,813,102],{"class":248},[132,815,477],{"class":149},[132,817,736],{"class":156},[132,819,483],{"class":149},[132,821,822],{"class":248},"_FILE are set; using ",[132,824,477],{"class":149},[132,826,736],{"class":156},[132,828,483],{"class":149},[132,830,778],{"class":248},[132,832,558],{"class":156},[132,834,562],{"class":561},[132,836,292],{"class":145},[132,838,839],{"class":149},"2",[132,841,570],{"class":156},[132,843,845,847],{"class":134,"line":844},51,[132,846,321],{"class":145},[132,848,849],{"class":156}," Secret(_read_file(Path(file_path)))\n",[132,851,853,855,858,860],{"class":134,"line":852},52,[132,854,500],{"class":145},[132,856,857],{"class":156}," value ",[132,859,762],{"class":145},[132,861,862],{"class":156}," os.environ.get(name):\n",[132,864,866,868],{"class":134,"line":865},53,[132,867,321],{"class":145},[132,869,870],{"class":156}," Secret(value.strip())\n",[132,872,874,876],{"class":134,"line":873},54,[132,875,500],{"class":145},[132,877,878],{"class":156}," required:\n",[132,880,882,884,886,888,891,893,895,897,900,902,904,906,909],{"class":134,"line":881},55,[132,883,465],{"class":145},[132,885,468],{"class":156},[132,887,471],{"class":145},[132,889,890],{"class":248},"\"set ",[132,892,477],{"class":149},[132,894,736],{"class":156},[132,896,483],{"class":149},[132,898,899],{"class":248}," or ",[132,901,477],{"class":149},[132,903,736],{"class":156},[132,905,483],{"class":149},[132,907,908],{"class":248},"_FILE (or pass it on stdin)\"",[132,910,570],{"class":156},[132,912,914,916],{"class":134,"line":913},56,[132,915,597],{"class":145},[132,917,494],{"class":149},[132,919,921],{"class":134,"line":920},57,[132,922,164],{"emptyLinePlaceholder":163},[132,924,926],{"class":134,"line":925},58,[132,927,164],{"emptyLinePlaceholder":163},[132,929,931,933,936,938,940,943,945,947,950,953,955,958,960,962,964,966,968,970,973,976,978,980,982],{"class":134,"line":930},59,[132,932,419],{"class":145},[132,934,935],{"class":229}," child_env",[132,937,391],{"class":156},[132,939,641],{"class":145},[132,941,942],{"class":156},", keep: tuple[",[132,944,272],{"class":149},[132,946,558],{"class":156},[132,948,949],{"class":149},"...",[132,951,952],{"class":156},"] ",[132,954,292],{"class":145},[132,956,957],{"class":156}," (), drop_prefixes: tuple[",[132,959,272],{"class":149},[132,961,558],{"class":156},[132,963,949],{"class":149},[132,965,952],{"class":156},[132,967,292],{"class":145},[132,969,245],{"class":156},[132,971,972],{"class":248},"\"MYTOOL_\"",[132,974,975],{"class":156},",)) -> dict[",[132,977,272],{"class":149},[132,979,558],{"class":156},[132,981,272],{"class":149},[132,983,984],{"class":156},"]:\n",[132,986,988],{"class":134,"line":987},60,[132,989,990],{"class":248},"    \"\"\"A copy of the environment without our secrets, for subprocesses.\"\"\"\n",[132,992,994,996],{"class":134,"line":993},61,[132,995,597],{"class":145},[132,997,998],{"class":156}," {\n",[132,1000,1002,1005,1008,1011,1013],{"class":134,"line":1001},62,[132,1003,1004],{"class":156},"        k: v ",[132,1006,1007],{"class":145},"for",[132,1009,1010],{"class":156}," k, v ",[132,1012,791],{"class":145},[132,1014,1015],{"class":156}," os.environ.items()\n",[132,1017,1019,1021,1024,1026,1029,1032,1034],{"class":134,"line":1018},63,[132,1020,712],{"class":145},[132,1022,1023],{"class":156}," k ",[132,1025,791],{"class":145},[132,1027,1028],{"class":156}," keep ",[132,1030,1031],{"class":145},"or",[132,1033,715],{"class":145},[132,1035,1036],{"class":156}," k.startswith(drop_prefixes)\n",[132,1038,1040],{"class":134,"line":1039},64,[132,1041,1042],{"class":156},"    }\n",[10,1044,1045],{},"And a command that uses it:",[123,1047,1049],{"className":125,"code":1048,"language":127,"meta":128,"style":128},"# src\u002Fmytool\u002Fcli.py\nimport subprocess\n\nimport typer\n\nfrom mytool.secrets import MissingSecret, child_env, read_secret\n\napp = typer.Typer()\n\n\n@app.callback()\ndef main() -> None:\n    \"\"\"Deployment tool.\"\"\"\n\n\n@app.command()\ndef deploy(\n    env: str,\n    token_stdin: bool = typer.Option(False, \"--token-stdin\", help=\"Read the API token from stdin.\"),\n) -> None:\n    \"\"\"Deploy the current build to ENV.\"\"\"\n    try:\n        token = read_secret(\"MYTOOL_TOKEN\", stdin=token_stdin)\n    except MissingSecret as exc:\n        typer.echo(f\"error: {exc}\", err=True)\n        raise typer.Exit(2)\n    headers = {\"Authorization\": f\"Bearer {token.reveal()}\"}\n    typer.echo(f\"deploying to {env} (auth header prepared: {len(headers)} header)\", err=True)\n    # Build tools run with our secrets removed from their environment.\n    subprocess.run([\"npm\", \"run\", \"build\"], env=child_env(), check=True)\n\n\nif __name__ == \"__main__\":\n    app()\n",[14,1050,1051,1056,1063,1067,1074,1078,1090,1094,1104,1108,1112,1120,1134,1139,1143,1147,1154,1164,1174,1207,1215,1220,1226,1249,1262,1293,1304,1337,1378,1383,1420,1424,1428,1444],{"__ignoreMap":128},[132,1052,1053],{"class":134,"line":135},[132,1054,1055],{"class":138},"# src\u002Fmytool\u002Fcli.py\n",[132,1057,1058,1060],{"class":134,"line":142},[132,1059,170],{"class":145},[132,1061,1062],{"class":156}," subprocess\n",[132,1064,1065],{"class":134,"line":160},[132,1066,164],{"emptyLinePlaceholder":163},[132,1068,1069,1071],{"class":134,"line":167},[132,1070,170],{"class":145},[132,1072,1073],{"class":156}," typer\n",[132,1075,1076],{"class":134,"line":176},[132,1077,164],{"emptyLinePlaceholder":163},[132,1079,1080,1082,1085,1087],{"class":134,"line":184},[132,1081,146],{"class":145},[132,1083,1084],{"class":156}," mytool.secrets ",[132,1086,170],{"class":145},[132,1088,1089],{"class":156}," MissingSecret, child_env, read_secret\n",[132,1091,1092],{"class":134,"line":192},[132,1093,164],{"emptyLinePlaceholder":163},[132,1095,1096,1099,1101],{"class":134,"line":200},[132,1097,1098],{"class":156},"app ",[132,1100,292],{"class":145},[132,1102,1103],{"class":156}," typer.Typer()\n",[132,1105,1106],{"class":134,"line":213},[132,1107,164],{"emptyLinePlaceholder":163},[132,1109,1110],{"class":134,"line":218},[132,1111,164],{"emptyLinePlaceholder":163},[132,1113,1114,1117],{"class":134,"line":223},[132,1115,1116],{"class":229},"@app.callback",[132,1118,1119],{"class":156},"()\n",[132,1121,1122,1124,1127,1130,1132],{"class":134,"line":236},[132,1123,419],{"class":145},[132,1125,1126],{"class":229}," main",[132,1128,1129],{"class":156},"() -> ",[132,1131,278],{"class":149},[132,1133,233],{"class":156},[132,1135,1136],{"class":134,"line":255},[132,1137,1138],{"class":248},"    \"\"\"Deployment tool.\"\"\"\n",[132,1140,1141],{"class":134,"line":260},[132,1142,164],{"emptyLinePlaceholder":163},[132,1144,1145],{"class":134,"line":283},[132,1146,164],{"emptyLinePlaceholder":163},[132,1148,1149,1152],{"class":134,"line":298},[132,1150,1151],{"class":229},"@app.command",[132,1153,1119],{"class":156},[132,1155,1156,1158,1161],{"class":134,"line":303},[132,1157,419],{"class":145},[132,1159,1160],{"class":229}," deploy",[132,1162,1163],{"class":156},"(\n",[132,1165,1166,1169,1171],{"class":134,"line":318},[132,1167,1168],{"class":156},"    env: ",[132,1170,272],{"class":149},[132,1172,1173],{"class":156},",\n",[132,1175,1176,1179,1181,1183,1186,1189,1191,1194,1196,1199,1201,1204],{"class":134,"line":330},[132,1177,1178],{"class":156},"    token_stdin: ",[132,1180,647],{"class":149},[132,1182,242],{"class":145},[132,1184,1185],{"class":156}," typer.Option(",[132,1187,1188],{"class":149},"False",[132,1190,558],{"class":156},[132,1192,1193],{"class":248},"\"--token-stdin\"",[132,1195,558],{"class":156},[132,1197,1198],{"class":561},"help",[132,1200,292],{"class":145},[132,1202,1203],{"class":248},"\"Read the API token from stdin.\"",[132,1205,1206],{"class":156},"),\n",[132,1208,1209,1211,1213],{"class":134,"line":335},[132,1210,275],{"class":156},[132,1212,278],{"class":149},[132,1214,233],{"class":156},[132,1216,1217],{"class":134,"line":349},[132,1218,1219],{"class":248},"    \"\"\"Deploy the current build to ENV.\"\"\"\n",[132,1221,1222,1224],{"class":134,"line":357},[132,1223,435],{"class":145},[132,1225,233],{"class":156},[132,1227,1228,1231,1233,1236,1239,1241,1244,1246],{"class":134,"line":362},[132,1229,1230],{"class":156},"        token ",[132,1232,292],{"class":145},[132,1234,1235],{"class":156}," read_secret(",[132,1237,1238],{"class":248},"\"MYTOOL_TOKEN\"",[132,1240,558],{"class":156},[132,1242,1243],{"class":561},"stdin",[132,1245,292],{"class":145},[132,1247,1248],{"class":156},"token_stdin)\n",[132,1250,1251,1253,1256,1259],{"class":134,"line":373},[132,1252,454],{"class":145},[132,1254,1255],{"class":156}," MissingSecret ",[132,1257,1258],{"class":145},"as",[132,1260,1261],{"class":156}," exc:\n",[132,1263,1264,1267,1269,1272,1274,1277,1279,1281,1283,1286,1288,1291],{"class":134,"line":378},[132,1265,1266],{"class":156},"        typer.echo(",[132,1268,471],{"class":145},[132,1270,1271],{"class":248},"\"error: ",[132,1273,477],{"class":149},[132,1275,1276],{"class":156},"exc",[132,1278,483],{"class":149},[132,1280,546],{"class":248},[132,1282,558],{"class":156},[132,1284,1285],{"class":561},"err",[132,1287,292],{"class":145},[132,1289,1290],{"class":149},"True",[132,1292,570],{"class":156},[132,1294,1295,1297,1300,1302],{"class":134,"line":383},[132,1296,465],{"class":145},[132,1298,1299],{"class":156}," typer.Exit(",[132,1301,839],{"class":149},[132,1303,570],{"class":156},[132,1305,1306,1309,1311,1314,1317,1320,1322,1325,1327,1330,1332,1334],{"class":134,"line":400},[132,1307,1308],{"class":156},"    headers ",[132,1310,292],{"class":145},[132,1312,1313],{"class":156}," {",[132,1315,1316],{"class":248},"\"Authorization\"",[132,1318,1319],{"class":156},": ",[132,1321,471],{"class":145},[132,1323,1324],{"class":248},"\"Bearer ",[132,1326,477],{"class":149},[132,1328,1329],{"class":156},"token.reveal()",[132,1331,483],{"class":149},[132,1333,546],{"class":248},[132,1335,1336],{"class":156},"}\n",[132,1338,1339,1342,1344,1347,1349,1352,1354,1357,1360,1363,1365,1368,1370,1372,1374,1376],{"class":134,"line":406},[132,1340,1341],{"class":156},"    typer.echo(",[132,1343,471],{"class":145},[132,1345,1346],{"class":248},"\"deploying to ",[132,1348,477],{"class":149},[132,1350,1351],{"class":156},"env",[132,1353,483],{"class":149},[132,1355,1356],{"class":248}," (auth header prepared: ",[132,1358,1359],{"class":149},"{len",[132,1361,1362],{"class":156},"(headers)",[132,1364,483],{"class":149},[132,1366,1367],{"class":248}," header)\"",[132,1369,558],{"class":156},[132,1371,1285],{"class":561},[132,1373,292],{"class":145},[132,1375,1290],{"class":149},[132,1377,570],{"class":156},[132,1379,1380],{"class":134,"line":411},[132,1381,1382],{"class":138},"    # Build tools run with our secrets removed from their environment.\n",[132,1384,1385,1388,1391,1393,1396,1398,1401,1404,1406,1408,1411,1414,1416,1418],{"class":134,"line":416},[132,1386,1387],{"class":156},"    subprocess.run([",[132,1389,1390],{"class":248},"\"npm\"",[132,1392,558],{"class":156},[132,1394,1395],{"class":248},"\"run\"",[132,1397,558],{"class":156},[132,1399,1400],{"class":248},"\"build\"",[132,1402,1403],{"class":156},"], ",[132,1405,1351],{"class":561},[132,1407,292],{"class":145},[132,1409,1410],{"class":156},"child_env(), ",[132,1412,1413],{"class":561},"check",[132,1415,292],{"class":145},[132,1417,1290],{"class":149},[132,1419,570],{"class":156},[132,1421,1422],{"class":134,"line":432},[132,1423,164],{"emptyLinePlaceholder":163},[132,1425,1426],{"class":134,"line":440},[132,1427,164],{"emptyLinePlaceholder":163},[132,1429,1430,1433,1436,1439,1442],{"class":134,"line":451},[132,1431,1432],{"class":145},"if",[132,1434,1435],{"class":149}," __name__",[132,1437,1438],{"class":145}," ==",[132,1440,1441],{"class":248}," \"__main__\"",[132,1443,233],{"class":156},[132,1445,1446],{"class":134,"line":462},[132,1447,1448],{"class":156},"    app()\n",[1450,1451,1453],"h3",{"id":1452},"the-details-that-matter","The details that matter",[10,1455,1456,1460,1461,1464,1465,1468,1469,1472],{},[1457,1458,1459],"strong",{},"Strip the trailing newline — and only that."," ",[14,1462,1463],{},"echo \"$TOKEN\" > token.txt",", most editors and many secret managers add a final newline. A token with ",[14,1466,1467],{},"\\n"," on the end fails authentication with a baffling \"invalid token\" error. ",[14,1470,1471],{},"rstrip(\"\\r\\n\")"," removes line endings without touching legitimate whitespace inside a password.",[10,1474,1475,1481],{},[1457,1476,1477,1480],{},[14,1478,1479],{},"_FILE"," wins over the plain variable."," When both are set, the file is almost certainly the operator's deliberate choice (a mounted secret) and the variable a leftover default. Warn so the ambiguity is visible.",[10,1483,1484,1487,1488,1491],{},[1457,1485,1486],{},"Check permissions on POSIX."," A secret file readable by the group or others is a misconfiguration worth flagging. A warning rather than a refusal keeps the tool usable on platforms and mounts where permission bits are not meaningful — some container volume types report ",[14,1489,1490],{},"0644"," regardless.",[10,1493,1494,1460,1497,1500,1501,558,1504,558,1507,1510,1511,31],{},[1457,1495,1496],{},"Scrub the environment for children.",[14,1498,1499],{},"child_env()"," removes your tool's own variables before running subprocesses. Third-party build scripts, package install hooks and linters do not need your deploy token. For stricter isolation, build the child environment from an allow-list instead — ",[14,1502,1503],{},"PATH",[14,1505,1506],{},"HOME",[14,1508,1509],{},"LANG"," and whatever the child actually needs — as discussed in ",[27,1512,1514],{"href":1513},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess\u002F","calling external commands safely with subprocess",[10,1516,1517,1520,1521,1524,1525,1528,1529,1532],{},[1457,1518,1519],{},"Wrap immediately."," The value is a ",[14,1522,1523],{},"Secret"," from the moment it is read, so a stray ",[14,1526,1527],{},"print(token)"," or a traceback prints asterisks. ",[14,1530,1531],{},"reveal()"," appears exactly where the header is built.",[33,1534,1536],{"id":1535},"ux-considerations","UX considerations",[38,1538,1539,1553,1565,1579,1592],{},[41,1540,1541,1544,1545,1548,1549,1552],{},[1457,1542,1543],{},"Name every option in the error."," \"set MYTOOL_TOKEN or MYTOOL_TOKEN_FILE (or pass it on stdin)\" tells a CI engineer exactly what to configure. List these variables in ",[14,1546,1547],{},"--help"," too — Typer's ",[14,1550,1551],{},"envvar="," shows them automatically for options that support it.",[41,1554,1555,1560,1561,1564],{},[1457,1556,1557,1558,31],{},"Offer ",[14,1559,24],{}," It lets users pipe from a password manager (",[14,1562,1563],{},"pass show mytool | mytool deploy prod --token-stdin",") without the secret touching argv or the environment.",[41,1566,1567,1570,1571,1575,1576,1578],{},[1457,1568,1569],{},"Document the precedence"," alongside your other settings, using the same order as in ",[27,1572,1574],{"href":1573},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults\u002F","config precedence: flags, env, files and defaults",": explicit flag, then ",[14,1577,1479],{},", then plain variable, then stored credential.",[41,1580,1581,1588,1589,1591],{},[1457,1582,1583,1584,1587],{},"Do not load secrets from ",[14,1585,1586],{},".env"," files implicitly."," Auto-loading a ",[14,1590,1586],{}," from the current directory is convenient for development and surprising in production, where a stray file can override a real secret. If you support it, make it opt-in with a flag.",[41,1593,1594,1597],{},[1457,1595,1596],{},"Never echo the value in errors."," \"token rejected\" and \"token from MYTOOL_TOKEN_FILE (\u002Frun\u002Fsecrets\u002Fmytool) was rejected\" are both fine; including the token is not.",[33,1599,1601],{"id":1600},"testing-the-behaviour","Testing the behaviour",[10,1603,1604,1607,1608,1611],{},[14,1605,1606],{},"monkeypatch"," controls the environment and ",[14,1609,1610],{},"tmp_path"," provides files with known permissions, so every branch is testable in isolation:",[123,1613,1615],{"className":125,"code":1614,"language":127,"meta":128,"style":128},"# tests\u002Ftest_secrets.py\nimport os\nimport stat\nimport sys\n\nimport pytest\n\nfrom mytool.secrets import MissingSecret, child_env, read_secret\n\n\n@pytest.fixture(autouse=True)\ndef clean_env(monkeypatch):\n    for var in (\"MYTOOL_TOKEN\", \"MYTOOL_TOKEN_FILE\"):\n        monkeypatch.delenv(var, raising=False)\n\n\ndef test_plain_variable(monkeypatch):\n    monkeypatch.setenv(\"MYTOOL_TOKEN\", \"abc123\")\n    assert read_secret(\"MYTOOL_TOKEN\").reveal() == \"abc123\"\n\n\ndef test_file_strips_newline(monkeypatch, tmp_path):\n    f = tmp_path \u002F \"token\"\n    f.write_text(\"s3cret-with-trailing-newline\\n\")\n    f.chmod(0o600)\n    monkeypatch.setenv(\"MYTOOL_TOKEN_FILE\", str(f))\n    assert read_secret(\"MYTOOL_TOKEN\").reveal() == \"s3cret-with-trailing-newline\"\n\n\ndef test_file_beats_variable_with_warning(monkeypatch, tmp_path):\n    f = tmp_path \u002F \"token\"\n    f.write_text(\"from-file\")\n    f.chmod(0o600)\n    monkeypatch.setenv(\"MYTOOL_TOKEN_FILE\", str(f))\n    monkeypatch.setenv(\"MYTOOL_TOKEN\", \"from-env\")\n    with pytest.warns(UserWarning, match=\"both\"):\n        assert read_secret(\"MYTOOL_TOKEN\").reveal() == \"from-file\"\n\n\n@pytest.mark.skipif(sys.platform == \"win32\", reason=\"POSIX permissions\")\ndef test_world_readable_file_warns(monkeypatch, tmp_path):\n    f = tmp_path \u002F \"token\"\n    f.write_text(\"x\")\n    f.chmod(0o644)\n    monkeypatch.setenv(\"MYTOOL_TOKEN_FILE\", str(f))\n    with pytest.warns(UserWarning, match=\"chmod 600\"):\n        read_secret(\"MYTOOL_TOKEN\")\n\n\ndef test_missing_names_both_options():\n    with pytest.raises(MissingSecret, match=\"MYTOOL_TOKEN_FILE\"):\n        read_secret(\"MYTOOL_TOKEN\")\n\n\ndef test_child_env_drops_our_secrets(monkeypatch):\n    monkeypatch.setenv(\"MYTOOL_TOKEN\", \"abc\")\n    monkeypatch.setenv(\"PATH\", os.environ.get(\"PATH\", \"\u002Fusr\u002Fbin\"))\n    env = child_env()\n    assert \"MYTOOL_TOKEN\" not in env and \"PATH\" in env\n\n\ndef test_secret_repr_is_masked(monkeypatch):\n    monkeypatch.setenv(\"MYTOOL_TOKEN\", \"abc123\")\n    assert \"abc123\" not in repr(read_secret(\"MYTOOL_TOKEN\"))\n",[14,1616,1617,1622,1628,1634,1640,1644,1651,1655,1665,1669,1673,1689,1699,1720,1734,1738,1742,1751,1765,1782,1786,1790,1800,1816,1830,1843,1856,1871,1875,1879,1888,1900,1909,1919,1931,1944,1967,1983,1987,1991,2016,2025,2037,2046,2057,2069,2088,2097,2101,2105,2115,2130,2138,2142,2146,2155,2168,2188,2198,2224,2228,2232,2241,2253],{"__ignoreMap":128},[132,1618,1619],{"class":134,"line":135},[132,1620,1621],{"class":138},"# tests\u002Ftest_secrets.py\n",[132,1623,1624,1626],{"class":134,"line":142},[132,1625,170],{"class":145},[132,1627,173],{"class":156},[132,1629,1630,1632],{"class":134,"line":160},[132,1631,170],{"class":145},[132,1633,181],{"class":156},[132,1635,1636,1638],{"class":134,"line":167},[132,1637,170],{"class":145},[132,1639,189],{"class":156},[132,1641,1642],{"class":134,"line":176},[132,1643,164],{"emptyLinePlaceholder":163},[132,1645,1646,1648],{"class":134,"line":184},[132,1647,170],{"class":145},[132,1649,1650],{"class":156}," pytest\n",[132,1652,1653],{"class":134,"line":192},[132,1654,164],{"emptyLinePlaceholder":163},[132,1656,1657,1659,1661,1663],{"class":134,"line":200},[132,1658,146],{"class":145},[132,1660,1084],{"class":156},[132,1662,170],{"class":145},[132,1664,1089],{"class":156},[132,1666,1667],{"class":134,"line":213},[132,1668,164],{"emptyLinePlaceholder":163},[132,1670,1671],{"class":134,"line":218},[132,1672,164],{"emptyLinePlaceholder":163},[132,1674,1675,1678,1680,1683,1685,1687],{"class":134,"line":223},[132,1676,1677],{"class":229},"@pytest.fixture",[132,1679,391],{"class":156},[132,1681,1682],{"class":561},"autouse",[132,1684,292],{"class":145},[132,1686,1290],{"class":149},[132,1688,570],{"class":156},[132,1690,1691,1693,1696],{"class":134,"line":236},[132,1692,419],{"class":145},[132,1694,1695],{"class":229}," clean_env",[132,1697,1698],{"class":156},"(monkeypatch):\n",[132,1700,1701,1704,1707,1709,1711,1713,1715,1718],{"class":134,"line":255},[132,1702,1703],{"class":145},"    for",[132,1705,1706],{"class":156}," var ",[132,1708,791],{"class":145},[132,1710,245],{"class":156},[132,1712,1238],{"class":248},[132,1714,558],{"class":156},[132,1716,1717],{"class":248},"\"MYTOOL_TOKEN_FILE\"",[132,1719,397],{"class":156},[132,1721,1722,1725,1728,1730,1732],{"class":134,"line":260},[132,1723,1724],{"class":156},"        monkeypatch.delenv(var, ",[132,1726,1727],{"class":561},"raising",[132,1729,292],{"class":145},[132,1731,1188],{"class":149},[132,1733,570],{"class":156},[132,1735,1736],{"class":134,"line":283},[132,1737,164],{"emptyLinePlaceholder":163},[132,1739,1740],{"class":134,"line":298},[132,1741,164],{"emptyLinePlaceholder":163},[132,1743,1744,1746,1749],{"class":134,"line":303},[132,1745,419],{"class":145},[132,1747,1748],{"class":229}," test_plain_variable",[132,1750,1698],{"class":156},[132,1752,1753,1756,1758,1760,1763],{"class":134,"line":318},[132,1754,1755],{"class":156},"    monkeypatch.setenv(",[132,1757,1238],{"class":248},[132,1759,558],{"class":156},[132,1761,1762],{"class":248},"\"abc123\"",[132,1764,570],{"class":156},[132,1766,1767,1770,1772,1774,1777,1779],{"class":134,"line":330},[132,1768,1769],{"class":145},"    assert",[132,1771,1235],{"class":156},[132,1773,1238],{"class":248},[132,1775,1776],{"class":156},").reveal() ",[132,1778,506],{"class":145},[132,1780,1781],{"class":248}," \"abc123\"\n",[132,1783,1784],{"class":134,"line":335},[132,1785,164],{"emptyLinePlaceholder":163},[132,1787,1788],{"class":134,"line":349},[132,1789,164],{"emptyLinePlaceholder":163},[132,1791,1792,1794,1797],{"class":134,"line":357},[132,1793,419],{"class":145},[132,1795,1796],{"class":229}," test_file_strips_newline",[132,1798,1799],{"class":156},"(monkeypatch, tmp_path):\n",[132,1801,1802,1805,1807,1810,1813],{"class":134,"line":362},[132,1803,1804],{"class":156},"    f ",[132,1806,292],{"class":145},[132,1808,1809],{"class":156}," tmp_path ",[132,1811,1812],{"class":145},"\u002F",[132,1814,1815],{"class":248}," \"token\"\n",[132,1817,1818,1821,1824,1826,1828],{"class":134,"line":373},[132,1819,1820],{"class":156},"    f.write_text(",[132,1822,1823],{"class":248},"\"s3cret-with-trailing-newline",[132,1825,1467],{"class":149},[132,1827,546],{"class":248},[132,1829,570],{"class":156},[132,1831,1832,1835,1838,1841],{"class":134,"line":378},[132,1833,1834],{"class":156},"    f.chmod(",[132,1836,1837],{"class":145},"0o",[132,1839,1840],{"class":149},"600",[132,1842,570],{"class":156},[132,1844,1845,1847,1849,1851,1853],{"class":134,"line":383},[132,1846,1755],{"class":156},[132,1848,1717],{"class":248},[132,1850,558],{"class":156},[132,1852,272],{"class":149},[132,1854,1855],{"class":156},"(f))\n",[132,1857,1858,1860,1862,1864,1866,1868],{"class":134,"line":400},[132,1859,1769],{"class":145},[132,1861,1235],{"class":156},[132,1863,1238],{"class":248},[132,1865,1776],{"class":156},[132,1867,506],{"class":145},[132,1869,1870],{"class":248}," \"s3cret-with-trailing-newline\"\n",[132,1872,1873],{"class":134,"line":406},[132,1874,164],{"emptyLinePlaceholder":163},[132,1876,1877],{"class":134,"line":411},[132,1878,164],{"emptyLinePlaceholder":163},[132,1880,1881,1883,1886],{"class":134,"line":416},[132,1882,419],{"class":145},[132,1884,1885],{"class":229}," test_file_beats_variable_with_warning",[132,1887,1799],{"class":156},[132,1889,1890,1892,1894,1896,1898],{"class":134,"line":432},[132,1891,1804],{"class":156},[132,1893,292],{"class":145},[132,1895,1809],{"class":156},[132,1897,1812],{"class":145},[132,1899,1815],{"class":248},[132,1901,1902,1904,1907],{"class":134,"line":440},[132,1903,1820],{"class":156},[132,1905,1906],{"class":248},"\"from-file\"",[132,1908,570],{"class":156},[132,1910,1911,1913,1915,1917],{"class":134,"line":451},[132,1912,1834],{"class":156},[132,1914,1837],{"class":145},[132,1916,1840],{"class":149},[132,1918,570],{"class":156},[132,1920,1921,1923,1925,1927,1929],{"class":134,"line":462},[132,1922,1755],{"class":156},[132,1924,1717],{"class":248},[132,1926,558],{"class":156},[132,1928,272],{"class":149},[132,1930,1855],{"class":156},[132,1932,1933,1935,1937,1939,1942],{"class":134,"line":497},[132,1934,1755],{"class":156},[132,1936,1238],{"class":248},[132,1938,558],{"class":156},[132,1940,1941],{"class":248},"\"from-env\"",[132,1943,570],{"class":156},[132,1945,1946,1949,1952,1955,1957,1960,1962,1965],{"class":134,"line":538},[132,1947,1948],{"class":145},"    with",[132,1950,1951],{"class":156}," pytest.warns(",[132,1953,1954],{"class":149},"UserWarning",[132,1956,558],{"class":156},[132,1958,1959],{"class":561},"match",[132,1961,292],{"class":145},[132,1963,1964],{"class":248},"\"both\"",[132,1966,397],{"class":156},[132,1968,1969,1972,1974,1976,1978,1980],{"class":134,"line":573},[132,1970,1971],{"class":145},"        assert",[132,1973,1235],{"class":156},[132,1975,1238],{"class":248},[132,1977,1776],{"class":156},[132,1979,506],{"class":145},[132,1981,1982],{"class":248}," \"from-file\"\n",[132,1984,1985],{"class":134,"line":594},[132,1986,164],{"emptyLinePlaceholder":163},[132,1988,1989],{"class":134,"line":616},[132,1990,164],{"emptyLinePlaceholder":163},[132,1992,1993,1996,1999,2001,2004,2006,2009,2011,2014],{"class":134,"line":621},[132,1994,1995],{"class":229},"@pytest.mark.skipif",[132,1997,1998],{"class":156},"(sys.platform ",[132,2000,506],{"class":145},[132,2002,2003],{"class":248}," \"win32\"",[132,2005,558],{"class":156},[132,2007,2008],{"class":561},"reason",[132,2010,292],{"class":145},[132,2012,2013],{"class":248},"\"POSIX permissions\"",[132,2015,570],{"class":156},[132,2017,2018,2020,2023],{"class":134,"line":626},[132,2019,419],{"class":145},[132,2021,2022],{"class":229}," test_world_readable_file_warns",[132,2024,1799],{"class":156},[132,2026,2027,2029,2031,2033,2035],{"class":134,"line":676},[132,2028,1804],{"class":156},[132,2030,292],{"class":145},[132,2032,1809],{"class":156},[132,2034,1812],{"class":145},[132,2036,1815],{"class":248},[132,2038,2039,2041,2044],{"class":134,"line":682},[132,2040,1820],{"class":156},[132,2042,2043],{"class":248},"\"x\"",[132,2045,570],{"class":156},[132,2047,2048,2050,2052,2055],{"class":134,"line":690},[132,2049,1834],{"class":156},[132,2051,1837],{"class":145},[132,2053,2054],{"class":149},"644",[132,2056,570],{"class":156},[132,2058,2059,2061,2063,2065,2067],{"class":134,"line":709},[132,2060,1755],{"class":156},[132,2062,1717],{"class":248},[132,2064,558],{"class":156},[132,2066,272],{"class":149},[132,2068,1855],{"class":156},[132,2070,2071,2073,2075,2077,2079,2081,2083,2086],{"class":134,"line":721},[132,2072,1948],{"class":145},[132,2074,1951],{"class":156},[132,2076,1954],{"class":149},[132,2078,558],{"class":156},[132,2080,1959],{"class":561},[132,2082,292],{"class":145},[132,2084,2085],{"class":248},"\"chmod 600\"",[132,2087,397],{"class":156},[132,2089,2090,2093,2095],{"class":134,"line":746},[132,2091,2092],{"class":156},"        read_secret(",[132,2094,1238],{"class":248},[132,2096,570],{"class":156},[132,2098,2099],{"class":134,"line":754},[132,2100,164],{"emptyLinePlaceholder":163},[132,2102,2103],{"class":134,"line":783},[132,2104,164],{"emptyLinePlaceholder":163},[132,2106,2107,2109,2112],{"class":134,"line":797},[132,2108,419],{"class":145},[132,2110,2111],{"class":229}," test_missing_names_both_options",[132,2113,2114],{"class":156},"():\n",[132,2116,2117,2119,2122,2124,2126,2128],{"class":134,"line":844},[132,2118,1948],{"class":145},[132,2120,2121],{"class":156}," pytest.raises(MissingSecret, ",[132,2123,1959],{"class":561},[132,2125,292],{"class":145},[132,2127,1717],{"class":248},[132,2129,397],{"class":156},[132,2131,2132,2134,2136],{"class":134,"line":852},[132,2133,2092],{"class":156},[132,2135,1238],{"class":248},[132,2137,570],{"class":156},[132,2139,2140],{"class":134,"line":865},[132,2141,164],{"emptyLinePlaceholder":163},[132,2143,2144],{"class":134,"line":873},[132,2145,164],{"emptyLinePlaceholder":163},[132,2147,2148,2150,2153],{"class":134,"line":881},[132,2149,419],{"class":145},[132,2151,2152],{"class":229}," test_child_env_drops_our_secrets",[132,2154,1698],{"class":156},[132,2156,2157,2159,2161,2163,2166],{"class":134,"line":913},[132,2158,1755],{"class":156},[132,2160,1238],{"class":248},[132,2162,558],{"class":156},[132,2164,2165],{"class":248},"\"abc\"",[132,2167,570],{"class":156},[132,2169,2170,2172,2175,2178,2180,2182,2185],{"class":134,"line":920},[132,2171,1755],{"class":156},[132,2173,2174],{"class":248},"\"PATH\"",[132,2176,2177],{"class":156},", os.environ.get(",[132,2179,2174],{"class":248},[132,2181,558],{"class":156},[132,2183,2184],{"class":248},"\"\u002Fusr\u002Fbin\"",[132,2186,2187],{"class":156},"))\n",[132,2189,2190,2193,2195],{"class":134,"line":925},[132,2191,2192],{"class":156},"    env ",[132,2194,292],{"class":145},[132,2196,2197],{"class":156}," child_env()\n",[132,2199,2200,2202,2205,2207,2210,2213,2216,2219,2221],{"class":134,"line":930},[132,2201,1769],{"class":145},[132,2203,2204],{"class":248}," \"MYTOOL_TOKEN\"",[132,2206,715],{"class":145},[132,2208,2209],{"class":145}," in",[132,2211,2212],{"class":156}," env ",[132,2214,2215],{"class":145},"and",[132,2217,2218],{"class":248}," \"PATH\"",[132,2220,2209],{"class":145},[132,2222,2223],{"class":156}," env\n",[132,2225,2226],{"class":134,"line":987},[132,2227,164],{"emptyLinePlaceholder":163},[132,2229,2230],{"class":134,"line":993},[132,2231,164],{"emptyLinePlaceholder":163},[132,2233,2234,2236,2239],{"class":134,"line":1001},[132,2235,419],{"class":145},[132,2237,2238],{"class":229}," test_secret_repr_is_masked",[132,2240,1698],{"class":156},[132,2242,2243,2245,2247,2249,2251],{"class":134,"line":1018},[132,2244,1755],{"class":156},[132,2246,1238],{"class":248},[132,2248,558],{"class":156},[132,2250,1762],{"class":248},[132,2252,570],{"class":156},[132,2254,2255,2257,2260,2262,2264,2267,2270,2272],{"class":134,"line":1039},[132,2256,1769],{"class":145},[132,2258,2259],{"class":248}," \"abc123\"",[132,2261,715],{"class":145},[132,2263,2209],{"class":145},[132,2265,2266],{"class":149}," repr",[132,2268,2269],{"class":156},"(read_secret(",[132,2271,1238],{"class":248},[132,2273,2187],{"class":156},[10,2275,2276,2277,2280,2281,2284,2285,2287,2288,2292],{},"For command-level tests, ",[14,2278,2279],{},"CliRunner.invoke(app, [...], env={\"MYTOOL_TOKEN\": \"x\"})"," sets variables for one invocation, and ",[14,2282,2283],{},"input=\"token\\n\""," feeds ",[14,2286,24],{},". See ",[27,2289,2291],{"href":2290},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin\u002F","testing interactive prompts and stdin"," for more on driving stdin in tests.",[33,2294,2296],{"id":2295},"conclusion","Conclusion",[10,2298,2299,2300,2302],{},"Automation delivers secrets through environment variables and mounted files, and a CLI that accepts both — plus stdin for piping from password managers — fits every platform without glue scripts. Support the ",[14,2301,1479],{}," convention, strip only the trailing newline, warn about loose permissions, wrap values in a masking type the moment you read them, and keep your secrets out of the environment of every child process. None of it is complicated; all of it prevents a real, recurring leak.",[33,2304,2306],{"id":2305},"frequently-asked-questions","Frequently asked questions",[1450,2308,2310,2311,2314],{"id":2309},"is-procpidenviron-a-risk","Is ",[14,2312,2313],{},"\u002Fproc\u002F\u003Cpid>\u002Fenviron"," a risk?",[10,2316,2317,2318,2321],{},"It is readable by the same user and root, not by other users, so an environment variable is protected to roughly the same degree as a ",[14,2319,2320],{},"0600"," file owned by that user. The bigger practical risk is inheritance into child processes, which is why scrubbing the child environment matters.",[1450,2323,2325],{"id":2324},"should-the-tool-delete-the-environment-variable-after-reading-it","Should the tool delete the environment variable after reading it?",[10,2327,2328,2329,2332,2333,2335],{},"Removing it from ",[14,2330,2331],{},"os.environ"," stops it being inherited by subprocesses you start later, which is a reasonable defensive step for long-running tools. It does not remove it from ",[14,2334,2313],{},", which reflects the environment at exec time.",[1450,2337,2339],{"id":2338},"how-do-i-support-secret-managers-like-1password-or-vault-directly","How do I support secret managers like 1Password or Vault directly?",[10,2341,2342,2343,558,2346,2349,2350,2352],{},"Usually you do not need to: their CLIs can inject secrets as environment variables (",[14,2344,2345],{},"op run",[14,2347,2348],{},"vault kv get -field=...",") or render files. Supporting ",[14,2351,1479],{}," and stdin covers them all without SDK dependencies.",[1450,2354,2356],{"id":2355},"what-about-secrets-in-config-files-the-user-writes","What about secrets in config files the user writes?",[10,2358,2359,2360,2363],{},"If you must allow it, read them but warn when the file is group- or world-readable, and never write secrets into config yourself. Prefer a reference instead — ",[14,2361,2362],{},"token_command = \"op read op:\u002F\u002Fvault\u002Fmytool\u002Ftoken\""," — which your tool runs to obtain the value at use time.",[33,2365,2367],{"id":2366},"related","Related",[38,2369,2370,2376,2381,2387,2393],{},[41,2371,2372,2373],{},"Up: ",[27,2374,2375],{"href":29},"Secrets and credentials in Python CLIs",[41,2377,2378],{},[27,2379,2380],{"href":52},"Storing tokens with keyring",[41,2382,2383],{},[27,2384,2386],{"href":2385},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely\u002F","Prompting for passwords securely",[41,2388,2389],{},[27,2390,2392],{"href":2391},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fredacting-secrets-from-cli-output-and-logs\u002F","Redacting secrets from CLI output and logs",[41,2394,2395],{},[27,2396,2398],{"href":2397},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fpublishing-to-pypi-with-trusted-publishing\u002F","Publishing to PyPI with trusted publishing",[2400,2401,2402],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":128,"searchDepth":142,"depth":142,"links":2404},[2405,2406,2408,2409,2412,2413,2414,2415,2422],{"id":35,"depth":142,"text":36},{"id":56,"depth":142,"text":2407},"Why not just a --token flag?",{"id":74,"depth":142,"text":75},{"id":120,"depth":142,"text":121,"children":2410},[2411],{"id":1452,"depth":160,"text":1453},{"id":1535,"depth":142,"text":1536},{"id":1600,"depth":142,"text":1601},{"id":2295,"depth":142,"text":2296},{"id":2305,"depth":142,"text":2306,"children":2416},[2417,2419,2420,2421],{"id":2309,"depth":160,"text":2418},"Is \u002Fproc\u002F\u003Cpid>\u002Fenviron a risk?",{"id":2324,"depth":160,"text":2325},{"id":2338,"depth":160,"text":2339},{"id":2355,"depth":160,"text":2356},{"id":2366,"depth":142,"text":2367},"2026-09-18","Accept credentials in a Python CLI the way CI and containers deliver them: env vars, the _FILE convention, stdin, permission checks and child-process hygiene.","intermediate",false,"md",{},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Freading-secrets-from-env-and-files",{"title":5,"description":2424},"cli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Freading-secrets-from-env-and-files\u002Findex",[2433,2434,2435,2436],"secrets","environment-variables","ci","containers","JB4Id2lm9cyHAaI_xJF5sJFNHjC5DIRi3SeXP4-YTH4",[2439,2442,2445,2448,2451,2454,2457,2460,2463,2466,2469,2472,2475,2478,2481,2484,2487,2490,2493,2496,2499,2502,2505,2508,2511,2514,2517,2520,2523,2526,2529,2532,2535,2538,2541,2544,2547,2550,2553,2556,2559,2562,2565,2568,2571,2574,2577,2580,2583,2586,2589,2592,2595,2598,2601,2604,2607,2610,2613,2616,2619,2622,2625,2628,2631,2634,2637,2640,2643,2646,2649,2652,2655,2658,2661,2664,2667,2670,2673,2676,2679,2682,2685,2688,2691,2694,2697,2700,2701,2704,2707,2710,2712,2715,2718,2721,2724,2727,2730,2733,2736,2739,2742,2745,2748,2751,2754,2757,2760,2763,2766,2769,2772,2775,2778,2781,2784,2787,2790,2793,2796,2799,2802,2805,2808,2811,2814,2817,2820,2823,2826,2829,2832,2835,2838,2841,2844,2847,2850,2853,2856,2859,2862,2865,2868,2871,2874,2877,2880,2883,2886,2889,2892,2895,2898,2901,2904,2907,2910,2913,2916,2919,2922,2925,2928,2931,2934,2937,2940,2943,2946,2949,2952,2955,2958,2961,2964,2967,2970,2973,2976,2979,2982],{"path":2440,"title":2441},"\u002Fabout","About Python CLI Toolcraft",{"path":2443,"title":2444},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":2446,"title":2447},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":2449,"title":2450},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":2452,"title":2453},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":2455,"title":2456},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":2458,"title":2459},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fbuilding-your-first-textual-app","Building Your First Textual App for a Python CLI",{"path":2461,"title":2462},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Fchoosing-between-a-cli-a-prompt-flow-and-a-tui","Choosing Between a CLI, a Prompt Flow and a TUI",{"path":2464,"title":2465},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":2467,"title":2468},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":2470,"title":2471},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fadding-examples-and-epilogs-to-help-output","Adding Examples and Epilogs to Help Output",{"path":2473,"title":2474},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fgenerating-man-pages-and-docs-from-a-cli","Generating Man Pages and Docs from a CLI",{"path":2476,"title":2477},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":2479,"title":2480},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":2482,"title":2483},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":2485,"title":2486},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":2488,"title":2489},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":2491,"title":2492},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Ffixing-unicode-and-encoding-errors-on-windows","Fixing Unicode and Encoding Errors on Windows in Python CLIs",{"path":2494,"title":2495},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":2497,"title":2498},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Frespecting-no-color-and-force-color","Respecting NO_COLOR and FORCE_COLOR in Python CLIs",{"path":2500,"title":2501},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2503,"title":2504},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fdesigning-an-exception-hierarchy-for-a-cli","Designing an Exception Hierarchy for a Python CLI",{"path":2506,"title":2507},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2509,"title":2510},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2512,"title":2513},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2515,"title":2516},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Freporting-machine-readable-errors-in-json-mode","Reporting Machine-Readable Errors in JSON Mode",{"path":2518,"title":2519},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2521,"title":2522},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fdiscovering-project-config-files-by-walking-up-directories","Discovering Project Config Files by Walking Up Directories",{"path":2524,"title":2525},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2527,"title":2528},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Floading-yaml-configs-safely-in-cli-apps","Loading YAML configs safely in CLI apps",{"path":2530,"title":2531},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":2533,"title":2534},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":2536,"title":2537},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2539,"title":2540},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fadding-progress-bars-and-spinners-to-python-clis","Progress Bars and Spinners for Python CLIs",{"path":2542,"title":2543},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":2545,"title":2546},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2548,"title":2549},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":2551,"title":2552},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":2554,"title":2555},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":2557,"title":2558},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fdynamic-completion-values-from-apis-and-files","Dynamic Completion Values from APIs and Files",{"path":2560,"title":2561},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Fenabling-tab-completion-in-click-and-typer","Enabling Tab Completion in Click and Typer",{"path":2563,"title":2564},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2566,"title":2567},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Finstalling-shell-completion-for-bash-zsh-fish","Installing Shell Completion for bash, zsh, fish",{"path":2569,"title":2570},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":2572,"title":2573},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-trace-ids-and-context-to-cli-logs","Adding Trace IDs and Context to Python CLI Logs",{"path":2575,"title":2576},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2578,"title":2579},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2581,"title":2582},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":2584,"title":2585},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fwriting-rotating-log-files-from-a-cli","Writing Rotating Log Files from a Python CLI",{"path":2587,"title":2588},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2590,"title":2591},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2593,"title":2594},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2596,"title":2597},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2599,"title":2600},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fprocessing-large-files-and-ndjson-streams","Processing Large Files and NDJSON Streams in Python CLIs",{"path":2602,"title":2603},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2605,"title":2606},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fbuilding-an-api-client-cli-with-httpx","Building an API Client CLI with httpx",{"path":2608,"title":2609},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":2611,"title":2612},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":2614,"title":2615},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2617,"title":2618},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2620,"title":2621},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fretries-and-backoff-for-cli-http-calls","Retries and Backoff for CLI HTTP Calls",{"path":2623,"title":2624},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fcancelling-async-tasks-on-ctrl-c","Cancelling Async Tasks on Ctrl+C in Python CLIs",{"path":2626,"title":2627},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2629,"title":2630},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2632,"title":2633},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2635,"title":2636},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2638,"title":2639},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frunning-async-code-in-typer-and-click","Running Async Code in Typer and Click",{"path":2641,"title":2642},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2644,"title":2645},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2647,"title":2648},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2650,"title":2651},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2653,"title":2654},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2656,"title":2657},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2659,"title":2660},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2662,"title":2663},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fbuilding-a-watch-mode-with-watchfiles","Building a Watch Mode with watchfiles in Python",{"path":2665,"title":2666},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2668,"title":2669},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhealth-checks-and-heartbeats-for-long-running-clis","Health Checks and Heartbeats for Long-Running CLIs",{"path":2671,"title":2672},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2674,"title":2675},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Frunning-a-cli-on-a-schedule-with-cron-and-systemd","Running a Python CLI on a Schedule with cron and systemd",{"path":2677,"title":2678},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2680,"title":2681},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2683,"title":2684},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2686,"title":2687},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2689,"title":2690},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2692,"title":2693},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fwrapping-git-and-other-tools-from-a-python-cli","Wrapping git and Other Tools from a Python CLI",{"path":2695,"title":2696},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":2698,"title":2699},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":2429,"title":5},{"path":2702,"title":2703},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fredacting-secrets-from-cli-output-and-logs","Redacting Secrets from CLI Output and Logs",{"path":2705,"title":2706},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2708,"title":2709},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":1812,"title":2711},"Python CLI Toolcraft",{"path":2713,"title":2714},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fcaching-expensive-work-between-cli-runs","Caching Expensive Work Between Python CLI Runs",{"path":2716,"title":2717},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2719,"title":2720},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2722,"title":2723},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2725,"title":2726},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2728,"title":2729},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2731,"title":2732},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2734,"title":2735},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2737,"title":2738},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2740,"title":2741},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2743,"title":2744},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2746,"title":2747},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fadding-dry-run-and-confirmation-to-destructive-commands","Adding Dry-Run and Confirmation to Destructive Commands",{"path":2749,"title":2750},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Ffollowing-posix-and-gnu-argument-conventions","Following POSIX and GNU Argument Conventions in Python",{"path":2752,"title":2753},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fglobal-options-vs-per-command-options","Global Options vs Per-Command Options in Python CLIs",{"path":2755,"title":2756},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2758,"title":2759},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2761,"title":2762},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2764,"title":2765},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2767,"title":2768},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2770,"title":2771},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2773,"title":2774},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2776,"title":2777},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fwriting-a-plugin-for-an-existing-cli","Writing a Plugin for an Existing CLI",{"path":2779,"title":2780},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fbest-practices-for-python-cli-entry-points","Best practices for Python CLI entry points",{"path":2782,"title":2783},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2785,"title":2786},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fhow-to-structure-a-large-python-cli-project","Structuring a Large Python CLI Project",{"path":2788,"title":2789},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2791,"title":2792},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2794,"title":2795},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2797,"title":2798},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fend-to-end-testing-an-installed-cli","End-to-End Testing an Installed Python CLI",{"path":2800,"title":2801},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2803,"title":2804},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2806,"title":2807},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmocking-filesystem-and-network-in-cli-tests","Mocking the Filesystem and Network in CLI Tests",{"path":2809,"title":2810},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2812,"title":2813},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2815,"title":2816},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2818,"title":2819},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2821,"title":2822},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-a-cli-with-subcommands-in-click","Building a CLI with subcommands in Click",{"path":2824,"title":2825},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2827,"title":2828},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fconverting-a-click-app-to-typer","Converting a Click App to Typer",{"path":2830,"title":2831},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2833,"title":2834},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2836,"title":2837},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2839,"title":2840},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2842,"title":2843},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2845,"title":2846},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2848,"title":2849},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fpublishing-to-pypi-with-trusted-publishing","Publishing a CLI to PyPI with Trusted Publishing",{"path":2851,"title":2852},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fsmoke-testing-the-built-wheel-in-ci","Smoke-Testing the Built Wheel of a Python CLI in CI",{"path":2854,"title":2855},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Ftesting-a-cli-across-python-versions-with-github-actions","Testing a CLI Across Python Versions in GitHub Actions",{"path":2857,"title":2858},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":2860,"title":2861},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2863,"title":2864},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2866,"title":2867},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2869,"title":2870},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2872,"title":2873},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2875,"title":2876},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2878,"title":2879},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2881,"title":2882},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2884,"title":2885},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fshipping-a-cli-as-a-zipapp-with-shiv","Shipping a CLI as a Zipapp with shiv",{"path":2887,"title":2888},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2890,"title":2891},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2893,"title":2894},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fenforcing-import-boundaries-in-a-cli-codebase","Enforcing Import Boundaries in a Python CLI Codebase",{"path":2896,"title":2897},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2899,"title":2900},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Ftype-checking-click-and-typer-code-with-mypy","Type-Checking Click and Typer Code with mypy",{"path":2902,"title":2903},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2905,"title":2906},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fderiving-versions-from-git-tags-with-hatch-vcs","Deriving CLI Versions from Git Tags with hatch-vcs",{"path":2908,"title":2909},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2911,"title":2912},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2914,"title":2915},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2917,"title":2918},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2920,"title":2921},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2923,"title":2924},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2926,"title":2927},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2929,"title":2930},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2932,"title":2933},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fwriting-pyproject-toml-metadata-for-a-cli","Writing pyproject.toml Metadata for a Python CLI",{"path":2935,"title":2936},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2938,"title":2939},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fmigrating-a-cli-from-poetry-to-uv","Migrating a Python CLI from Poetry to uv",{"path":2941,"title":2942},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2944,"title":2945},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2947,"title":2948},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2950,"title":2951},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fsetting-up-pre-commit-for-python-cli-repos","Setting up pre-commit for Python CLI repos",{"path":2953,"title":2954},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fshipping-your-cli-as-a-pre-commit-hook","Shipping Your Python CLI as a pre-commit Hook",{"path":2956,"title":2957},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects\u002Fwriting-local-pre-commit-hooks-in-python","Writing Local pre-commit Hooks in Python",{"path":2959,"title":2960},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2962,"title":2963},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Frunning-one-off-cli-scripts-with-uv-run","Running One-Off CLI Scripts with uv run and PEP 723",{"path":2965,"title":2966},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-init-vs-poetry-init-for-cli-tools","uv init vs poetry init for CLI tools",{"path":2968,"title":2969},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-tool-install-vs-pipx-for-clis","uv tool install vs pipx for CLIs",{"path":2971,"title":2972},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2974,"title":2975},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2977,"title":2978},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2980,"title":2981},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2983,"title":2984},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736905051]