[{"data":1,"prerenderedAt":2853},["ShallowReactive",2],{"page-\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fredacting-secrets-from-cli-output-and-logs\u002F":3,"content-directory":2305},{"id":4,"title":5,"body":6,"date":2291,"description":2292,"difficulty":2293,"draft":2294,"extension":2295,"meta":2296,"navigation":173,"path":2297,"seo":2298,"stem":2299,"tags":2300,"updated":2291,"__hash__":2304},"content\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fredacting-secrets-from-cli-output-and-logs\u002Findex.md","Redacting Secrets from CLI Output and Logs",{"type":7,"value":8,"toc":2270},"minimark",[9,28,33,60,64,80,84,87,90,128,132,1256,1259,1305,1308,1371,1376,1382,1535,1538,1542,1548,1609,1613,1665,1669,1672,2146,2149,2153,2162,2166,2170,2173,2177,2188,2196,2199,2203,2213,2221,2232,2236,2266],[10,11,12,13,17,18,21,22,27],"p",{},"A user hits a bug, reruns your CLI with ",[14,15,16],"code",{},"--debug"," as the issue template asks, and pastes the output into a public ticket. Line 14 is ",[14,19,20],{},"DEBUG httpx: Authorization: Bearer eyJhbGciOi..."," — a live production token, now indexed by search engines. Nobody did anything wrong: the debug output was designed to be shared, and a library below your code printed a header you never touched. Secrets leak through output far more often than through storage, and the fix cannot rely on every call site being careful. This guide adds a redaction layer to your CLI's logging that masks registered secret values and common token patterns before anything is written, extends it to tracebacks, and tests that it works. It is part of the ",[23,24,26],"a",{"href":25},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002F","secrets and credentials topic",".",[29,30,32],"h2",{"id":31},"prerequisites","Prerequisites",[34,35,36,48],"ul",{},[37,38,39,40,43,44,27],"li",{},"Python 3.10+ and the standard ",[14,41,42],{},"logging"," module, configured once at startup as described in ",[23,45,47],{"href":46},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002F","structured logging for CLI apps",[37,49,50,51,54,55,59],{},"Secrets read through one place — the resolver from ",[23,52,53],{"href":25},"the topic overview"," or ",[23,56,58],{"href":57},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Freading-secrets-from-env-and-files\u002F","reading secrets from env and files"," — so they can be registered for masking.",[29,61,63],{"id":62},"where-to-redact","Where to redact",[10,65,66,67,71,72,75,76,79],{},"Redaction belongs at the ",[68,69,70],"strong",{},"last possible moment",": after a log record has been created by any module, including third-party libraries, and before any handler writes it anywhere. In Python's logging model, that is a ",[14,73,74],{},"logging.Filter"," attached to each ",[68,77,78],{},"handler",". A filter on a logger only sees records created by that logger; a filter on the handlers sees everything that reaches the output.",[81,82],"inline-diagram",{"name":83},"sec-redaction-flow",[10,85,86],{},"Two complementary techniques make up the filter:",[81,88],{"name":89},"sec-redaction-patterns",[34,91,92,102],{},[37,93,94,97,98,101],{},[68,95,96],{},"Known values."," Every secret your tool reads is registered with the redactor at the moment it is read. Any occurrence of that exact string in any output becomes ",[14,99,100],{},"****",". This catches your token no matter which library prints it or in which format.",[37,103,104,107,108,111,112,115,116,119,120,123,124,127],{},[68,105,106],{},"Patterns."," Some secrets you never see as values: tokens embedded in URLs from config, credentials in a library's own error message, headers built by an SDK. Regular expressions for ",[14,109,110],{},"Authorization"," headers, ",[14,113,114],{},"user:password@"," in URLs and well-known token prefixes (GitHub's ",[14,117,118],{},"ghp_",", Slack's ",[14,121,122],{},"xoxb-",", AWS access key IDs starting ",[14,125,126],{},"AKIA",") catch the common cases.",[29,129,131],{"id":130},"the-recipe","The recipe",[133,134,139],"pre",{"className":135,"code":136,"language":137,"meta":138,"style":138},"language-python shiki shiki-themes github-light github-dark","# src\u002Fmytool\u002Fredact.py\nfrom __future__ import annotations\n\nimport logging\nimport re\nimport threading\nfrom typing import TextIO\n\nMASK = \"****\"\n\nPATTERNS: list[tuple[re.Pattern[str], str]] = [\n    (re.compile(r\"(?i)(authorization:\\s*(?:bearer|basic|token)\\s+)\\S+\"), r\"\\1\" + MASK),\n    (re.compile(r\"(?i)\\b(bearer\\s+)[A-Za-z0-9._~+\u002F-]{8,}=*\"), r\"\\1\" + MASK),\n    (re.compile(r\"(:\u002F\u002F)[^\u002F\\s:@]+:[^\u002F\\s@]+@\"), r\"\\1\" + MASK + \"@\"),\n    (re.compile(r\"\\b(ghp|gho|ghs|ghu|github_pat)_[A-Za-z0-9_]{10,}\"), r\"\\1_\" + MASK),\n    (re.compile(r\"\\b(xox[abposr])-[A-Za-z0-9-]{10,}\"), r\"\\1-\" + MASK),\n    (re.compile(r\"\\bAKIA[0-9A-Z]{16}\\b\"), \"AKIA\" + MASK),\n    (re.compile(r\"(?i)((?:password|passwd|secret|token|api_key)\\s*[=:]\\s*)[^\\s,;&]+\"), r\"\\1\" + MASK),\n]\n\n\nclass Redactor:\n    \"\"\"Masks registered secret values and known token shapes in text.\"\"\"\n\n    def __init__(self) -> None:\n        self._values: set[str] = set()\n        self._lock = threading.Lock()\n\n    def register(self, value: str | None) -> None:\n        if value and len(value) >= 6:          # short values would mask ordinary words\n            with self._lock:\n                self._values.add(value)\n\n    def __call__(self, text: str) -> str:\n        with self._lock:\n            values = sorted(self._values, key=len, reverse=True)\n        for v in values:\n            text = text.replace(v, MASK)\n        for pattern, repl in PATTERNS:\n            text = pattern.sub(repl, text)\n        return text\n\n\nredactor = Redactor()\n\n\nclass RedactingFilter(logging.Filter):\n    def filter(self, record: logging.LogRecord) -> bool:\n        record.msg = redactor(record.getMessage())\n        record.args = None\n        if record.exc_info and not record.exc_text:\n            record.exc_text = logging.Formatter().formatException(record.exc_info)\n        if record.exc_text:\n            record.exc_text = redactor(record.exc_text)\n        return True\n\n\ndef install(level: int = logging.WARNING, stream: TextIO | None = None) -> None:\n    \"\"\"Configure root logging with redaction on every handler.\"\"\"\n    handler = logging.StreamHandler(stream)       # None means sys.stderr\n    handler.addFilter(RedactingFilter())\n    handler.setFormatter(logging.Formatter(\"%(levelname)s %(name)s: %(message)s\"))\n    root = logging.getLogger()\n    root.handlers[:] = [handler]\n    root.setLevel(level)\n","python","",[14,140,141,150,168,175,184,192,200,213,218,231,236,262,342,390,454,520,565,600,672,678,683,688,701,707,712,729,751,764,769,795,825,837,846,851,870,880,922,937,952,967,977,986,991,996,1007,1012,1017,1037,1053,1064,1075,1091,1102,1109,1119,1127,1132,1137,1177,1183,1197,1203,1228,1239,1250],{"__ignoreMap":138},[142,143,146],"span",{"class":144,"line":145},"line",1,[142,147,149],{"class":148},"sJ8bj","# src\u002Fmytool\u002Fredact.py\n",[142,151,153,157,161,164],{"class":144,"line":152},2,[142,154,156],{"class":155},"szBVR","from",[142,158,160],{"class":159},"sj4cs"," __future__",[142,162,163],{"class":155}," import",[142,165,167],{"class":166},"sVt8B"," annotations\n",[142,169,171],{"class":144,"line":170},3,[142,172,174],{"emptyLinePlaceholder":173},true,"\n",[142,176,178,181],{"class":144,"line":177},4,[142,179,180],{"class":155},"import",[142,182,183],{"class":166}," logging\n",[142,185,187,189],{"class":144,"line":186},5,[142,188,180],{"class":155},[142,190,191],{"class":166}," re\n",[142,193,195,197],{"class":144,"line":194},6,[142,196,180],{"class":155},[142,198,199],{"class":166}," threading\n",[142,201,203,205,208,210],{"class":144,"line":202},7,[142,204,156],{"class":155},[142,206,207],{"class":166}," typing ",[142,209,180],{"class":155},[142,211,212],{"class":166}," TextIO\n",[142,214,216],{"class":144,"line":215},8,[142,217,174],{"emptyLinePlaceholder":173},[142,219,221,224,227],{"class":144,"line":220},9,[142,222,223],{"class":159},"MASK",[142,225,226],{"class":155}," =",[142,228,230],{"class":229},"sZZnC"," \"****\"\n",[142,232,234],{"class":144,"line":233},10,[142,235,174],{"emptyLinePlaceholder":173},[142,237,239,242,245,248,251,253,256,259],{"class":144,"line":238},11,[142,240,241],{"class":159},"PATTERNS",[142,243,244],{"class":166},": list[tuple[re.Pattern[",[142,246,247],{"class":159},"str",[142,249,250],{"class":166},"], ",[142,252,247],{"class":159},[142,254,255],{"class":166},"]] ",[142,257,258],{"class":155},"=",[142,260,261],{"class":166}," [\n",[142,263,265,268,271,274,277,280,284,287,290,293,296,299,302,304,307,310,313,316,318,320,323,325,327,331,333,336,339],{"class":144,"line":264},12,[142,266,267],{"class":166},"    (re.compile(",[142,269,270],{"class":155},"r",[142,272,273],{"class":229},"\"",[142,275,276],{"class":155},"(?i)",[142,278,279],{"class":159},"(",[142,281,283],{"class":282},"sA_wV","authorization:",[142,285,286],{"class":159},"\\s",[142,288,289],{"class":155},"*",[142,291,292],{"class":159},"(?:",[142,294,295],{"class":282},"bearer",[142,297,298],{"class":155},"|",[142,300,301],{"class":282},"basic",[142,303,298],{"class":155},[142,305,306],{"class":282},"token",[142,308,309],{"class":159},")\\s",[142,311,312],{"class":155},"+",[142,314,315],{"class":159},")\\S",[142,317,312],{"class":155},[142,319,273],{"class":229},[142,321,322],{"class":166},"), ",[142,324,270],{"class":155},[142,326,273],{"class":229},[142,328,330],{"class":329},"s9eBZ","\\1",[142,332,273],{"class":229},[142,334,335],{"class":155}," +",[142,337,338],{"class":159}," MASK",[142,340,341],{"class":166},"),\n",[142,343,345,347,349,351,353,356,358,360,362,365,368,370,372,374,376,378,380,382,384,386,388],{"class":144,"line":344},13,[142,346,267],{"class":166},[142,348,270],{"class":155},[142,350,273],{"class":229},[142,352,276],{"class":155},[142,354,355],{"class":159},"\\b(",[142,357,295],{"class":282},[142,359,286],{"class":159},[142,361,312],{"class":155},[142,363,364],{"class":159},")[A-Za-z0-9._~+\u002F-]",[142,366,367],{"class":155},"{8,}",[142,369,258],{"class":282},[142,371,289],{"class":155},[142,373,273],{"class":229},[142,375,322],{"class":166},[142,377,270],{"class":155},[142,379,273],{"class":229},[142,381,330],{"class":329},[142,383,273],{"class":229},[142,385,335],{"class":155},[142,387,338],{"class":159},[142,389,341],{"class":166},[142,391,393,395,397,399,401,404,407,410,413,415,418,421,423,426,428,431,433,435,437,439,441,443,445,447,449,452],{"class":144,"line":392},14,[142,394,267],{"class":166},[142,396,270],{"class":155},[142,398,273],{"class":229},[142,400,279],{"class":159},[142,402,403],{"class":282},":\u002F\u002F",[142,405,406],{"class":159},")[",[142,408,409],{"class":155},"^",[142,411,412],{"class":159},"\u002F\\s:@]",[142,414,312],{"class":155},[142,416,417],{"class":282},":",[142,419,420],{"class":159},"[",[142,422,409],{"class":155},[142,424,425],{"class":159},"\u002F\\s@]",[142,427,312],{"class":155},[142,429,430],{"class":282},"@",[142,432,273],{"class":229},[142,434,322],{"class":166},[142,436,270],{"class":155},[142,438,273],{"class":229},[142,440,330],{"class":329},[142,442,273],{"class":229},[142,444,335],{"class":155},[142,446,338],{"class":159},[142,448,335],{"class":155},[142,450,451],{"class":229}," \"@\"",[142,453,341],{"class":166},[142,455,457,459,461,463,465,468,470,473,475,478,480,483,485,488,491,494,497,500,502,504,506,508,510,512,514,516,518],{"class":144,"line":456},15,[142,458,267],{"class":166},[142,460,270],{"class":155},[142,462,273],{"class":229},[142,464,355],{"class":159},[142,466,467],{"class":282},"ghp",[142,469,298],{"class":155},[142,471,472],{"class":282},"gho",[142,474,298],{"class":155},[142,476,477],{"class":282},"ghs",[142,479,298],{"class":155},[142,481,482],{"class":282},"ghu",[142,484,298],{"class":155},[142,486,487],{"class":282},"github_pat",[142,489,490],{"class":159},")",[142,492,493],{"class":282},"_",[142,495,496],{"class":159},"[A-Za-z0-9_]",[142,498,499],{"class":155},"{10,}",[142,501,273],{"class":229},[142,503,322],{"class":166},[142,505,270],{"class":155},[142,507,273],{"class":229},[142,509,330],{"class":329},[142,511,493],{"class":282},[142,513,273],{"class":229},[142,515,335],{"class":155},[142,517,338],{"class":159},[142,519,341],{"class":166},[142,521,523,525,527,529,531,534,537,540,543,545,547,549,551,553,555,557,559,561,563],{"class":144,"line":522},16,[142,524,267],{"class":166},[142,526,270],{"class":155},[142,528,273],{"class":229},[142,530,355],{"class":159},[142,532,533],{"class":282},"xox",[142,535,536],{"class":159},"[abposr])",[142,538,539],{"class":282},"-",[142,541,542],{"class":159},"[A-Za-z0-9-]",[142,544,499],{"class":155},[142,546,273],{"class":229},[142,548,322],{"class":166},[142,550,270],{"class":155},[142,552,273],{"class":229},[142,554,330],{"class":329},[142,556,539],{"class":282},[142,558,273],{"class":229},[142,560,335],{"class":155},[142,562,338],{"class":159},[142,564,341],{"class":166},[142,566,568,570,572,574,577,579,582,585,587,589,591,594,596,598],{"class":144,"line":567},17,[142,569,267],{"class":166},[142,571,270],{"class":155},[142,573,273],{"class":229},[142,575,576],{"class":159},"\\b",[142,578,126],{"class":282},[142,580,581],{"class":159},"[0-9A-Z]",[142,583,584],{"class":155},"{16}",[142,586,576],{"class":159},[142,588,273],{"class":229},[142,590,322],{"class":166},[142,592,593],{"class":229},"\"AKIA\"",[142,595,335],{"class":155},[142,597,338],{"class":159},[142,599,341],{"class":166},[142,601,603,605,607,609,611,614,617,619,622,624,627,629,631,633,636,638,640,643,645,647,649,652,654,656,658,660,662,664,666,668,670],{"class":144,"line":602},18,[142,604,267],{"class":166},[142,606,270],{"class":155},[142,608,273],{"class":229},[142,610,276],{"class":155},[142,612,613],{"class":159},"((?:",[142,615,616],{"class":282},"password",[142,618,298],{"class":155},[142,620,621],{"class":282},"passwd",[142,623,298],{"class":155},[142,625,626],{"class":282},"secret",[142,628,298],{"class":155},[142,630,306],{"class":282},[142,632,298],{"class":155},[142,634,635],{"class":282},"api_key",[142,637,309],{"class":159},[142,639,289],{"class":155},[142,641,642],{"class":159},"[=:]\\s",[142,644,289],{"class":155},[142,646,406],{"class":159},[142,648,409],{"class":155},[142,650,651],{"class":159},"\\s,;&]",[142,653,312],{"class":155},[142,655,273],{"class":229},[142,657,322],{"class":166},[142,659,270],{"class":155},[142,661,273],{"class":229},[142,663,330],{"class":329},[142,665,273],{"class":229},[142,667,335],{"class":155},[142,669,338],{"class":159},[142,671,341],{"class":166},[142,673,675],{"class":144,"line":674},19,[142,676,677],{"class":166},"]\n",[142,679,681],{"class":144,"line":680},20,[142,682,174],{"emptyLinePlaceholder":173},[142,684,686],{"class":144,"line":685},21,[142,687,174],{"emptyLinePlaceholder":173},[142,689,691,694,698],{"class":144,"line":690},22,[142,692,693],{"class":155},"class",[142,695,697],{"class":696},"sScJk"," Redactor",[142,699,700],{"class":166},":\n",[142,702,704],{"class":144,"line":703},23,[142,705,706],{"class":229},"    \"\"\"Masks registered secret values and known token shapes in text.\"\"\"\n",[142,708,710],{"class":144,"line":709},24,[142,711,174],{"emptyLinePlaceholder":173},[142,713,715,718,721,724,727],{"class":144,"line":714},25,[142,716,717],{"class":155},"    def",[142,719,720],{"class":159}," __init__",[142,722,723],{"class":166},"(self) -> ",[142,725,726],{"class":159},"None",[142,728,700],{"class":166},[142,730,732,735,738,740,743,745,748],{"class":144,"line":731},26,[142,733,734],{"class":159},"        self",[142,736,737],{"class":166},"._values: set[",[142,739,247],{"class":159},[142,741,742],{"class":166},"] ",[142,744,258],{"class":155},[142,746,747],{"class":159}," set",[142,749,750],{"class":166},"()\n",[142,752,754,756,759,761],{"class":144,"line":753},27,[142,755,734],{"class":159},[142,757,758],{"class":166},"._lock ",[142,760,258],{"class":155},[142,762,763],{"class":166}," threading.Lock()\n",[142,765,767],{"class":144,"line":766},28,[142,768,174],{"emptyLinePlaceholder":173},[142,770,772,774,777,780,782,785,788,791,793],{"class":144,"line":771},29,[142,773,717],{"class":155},[142,775,776],{"class":696}," register",[142,778,779],{"class":166},"(self, value: ",[142,781,247],{"class":159},[142,783,784],{"class":155}," |",[142,786,787],{"class":159}," None",[142,789,790],{"class":166},") -> ",[142,792,726],{"class":159},[142,794,700],{"class":166},[142,796,798,801,804,807,810,813,816,819,822],{"class":144,"line":797},30,[142,799,800],{"class":155},"        if",[142,802,803],{"class":166}," value ",[142,805,806],{"class":155},"and",[142,808,809],{"class":159}," len",[142,811,812],{"class":166},"(value) ",[142,814,815],{"class":155},">=",[142,817,818],{"class":159}," 6",[142,820,821],{"class":166},":          ",[142,823,824],{"class":148},"# short values would mask ordinary words\n",[142,826,828,831,834],{"class":144,"line":827},31,[142,829,830],{"class":155},"            with",[142,832,833],{"class":159}," self",[142,835,836],{"class":166},"._lock:\n",[142,838,840,843],{"class":144,"line":839},32,[142,841,842],{"class":159},"                self",[142,844,845],{"class":166},"._values.add(value)\n",[142,847,849],{"class":144,"line":848},33,[142,850,174],{"emptyLinePlaceholder":173},[142,852,854,856,859,862,864,866,868],{"class":144,"line":853},34,[142,855,717],{"class":155},[142,857,858],{"class":159}," __call__",[142,860,861],{"class":166},"(self, text: ",[142,863,247],{"class":159},[142,865,790],{"class":166},[142,867,247],{"class":159},[142,869,700],{"class":166},[142,871,873,876,878],{"class":144,"line":872},35,[142,874,875],{"class":155},"        with",[142,877,833],{"class":159},[142,879,836],{"class":166},[142,881,883,886,888,891,893,896,899,903,905,908,911,914,916,919],{"class":144,"line":882},36,[142,884,885],{"class":166},"            values ",[142,887,258],{"class":155},[142,889,890],{"class":159}," sorted",[142,892,279],{"class":166},[142,894,895],{"class":159},"self",[142,897,898],{"class":166},"._values, ",[142,900,902],{"class":901},"s4XuR","key",[142,904,258],{"class":155},[142,906,907],{"class":159},"len",[142,909,910],{"class":166},", ",[142,912,913],{"class":901},"reverse",[142,915,258],{"class":155},[142,917,918],{"class":159},"True",[142,920,921],{"class":166},")\n",[142,923,925,928,931,934],{"class":144,"line":924},37,[142,926,927],{"class":155},"        for",[142,929,930],{"class":166}," v ",[142,932,933],{"class":155},"in",[142,935,936],{"class":166}," values:\n",[142,938,940,943,945,948,950],{"class":144,"line":939},38,[142,941,942],{"class":166},"            text ",[142,944,258],{"class":155},[142,946,947],{"class":166}," text.replace(v, ",[142,949,223],{"class":159},[142,951,921],{"class":166},[142,953,955,957,960,962,965],{"class":144,"line":954},39,[142,956,927],{"class":155},[142,958,959],{"class":166}," pattern, repl ",[142,961,933],{"class":155},[142,963,964],{"class":159}," PATTERNS",[142,966,700],{"class":166},[142,968,970,972,974],{"class":144,"line":969},40,[142,971,942],{"class":166},[142,973,258],{"class":155},[142,975,976],{"class":166}," pattern.sub(repl, text)\n",[142,978,980,983],{"class":144,"line":979},41,[142,981,982],{"class":155},"        return",[142,984,985],{"class":166}," text\n",[142,987,989],{"class":144,"line":988},42,[142,990,174],{"emptyLinePlaceholder":173},[142,992,994],{"class":144,"line":993},43,[142,995,174],{"emptyLinePlaceholder":173},[142,997,999,1002,1004],{"class":144,"line":998},44,[142,1000,1001],{"class":166},"redactor ",[142,1003,258],{"class":155},[142,1005,1006],{"class":166}," Redactor()\n",[142,1008,1010],{"class":144,"line":1009},45,[142,1011,174],{"emptyLinePlaceholder":173},[142,1013,1015],{"class":144,"line":1014},46,[142,1016,174],{"emptyLinePlaceholder":173},[142,1018,1020,1022,1025,1027,1029,1031,1034],{"class":144,"line":1019},47,[142,1021,693],{"class":155},[142,1023,1024],{"class":696}," RedactingFilter",[142,1026,279],{"class":166},[142,1028,42],{"class":696},[142,1030,27],{"class":166},[142,1032,1033],{"class":696},"Filter",[142,1035,1036],{"class":166},"):\n",[142,1038,1040,1042,1045,1048,1051],{"class":144,"line":1039},48,[142,1041,717],{"class":155},[142,1043,1044],{"class":159}," filter",[142,1046,1047],{"class":166},"(self, record: logging.LogRecord) -> ",[142,1049,1050],{"class":159},"bool",[142,1052,700],{"class":166},[142,1054,1056,1059,1061],{"class":144,"line":1055},49,[142,1057,1058],{"class":166},"        record.msg ",[142,1060,258],{"class":155},[142,1062,1063],{"class":166}," redactor(record.getMessage())\n",[142,1065,1067,1070,1072],{"class":144,"line":1066},50,[142,1068,1069],{"class":166},"        record.args ",[142,1071,258],{"class":155},[142,1073,1074],{"class":159}," None\n",[142,1076,1078,1080,1083,1085,1088],{"class":144,"line":1077},51,[142,1079,800],{"class":155},[142,1081,1082],{"class":166}," record.exc_info ",[142,1084,806],{"class":155},[142,1086,1087],{"class":155}," not",[142,1089,1090],{"class":166}," record.exc_text:\n",[142,1092,1094,1097,1099],{"class":144,"line":1093},52,[142,1095,1096],{"class":166},"            record.exc_text ",[142,1098,258],{"class":155},[142,1100,1101],{"class":166}," logging.Formatter().formatException(record.exc_info)\n",[142,1103,1105,1107],{"class":144,"line":1104},53,[142,1106,800],{"class":155},[142,1108,1090],{"class":166},[142,1110,1112,1114,1116],{"class":144,"line":1111},54,[142,1113,1096],{"class":166},[142,1115,258],{"class":155},[142,1117,1118],{"class":166}," redactor(record.exc_text)\n",[142,1120,1122,1124],{"class":144,"line":1121},55,[142,1123,982],{"class":155},[142,1125,1126],{"class":159}," True\n",[142,1128,1130],{"class":144,"line":1129},56,[142,1131,174],{"emptyLinePlaceholder":173},[142,1133,1135],{"class":144,"line":1134},57,[142,1136,174],{"emptyLinePlaceholder":173},[142,1138,1140,1143,1146,1149,1152,1154,1157,1160,1163,1165,1167,1169,1171,1173,1175],{"class":144,"line":1139},58,[142,1141,1142],{"class":155},"def",[142,1144,1145],{"class":696}," install",[142,1147,1148],{"class":166},"(level: ",[142,1150,1151],{"class":159},"int",[142,1153,226],{"class":155},[142,1155,1156],{"class":166}," logging.",[142,1158,1159],{"class":159},"WARNING",[142,1161,1162],{"class":166},", stream: TextIO ",[142,1164,298],{"class":155},[142,1166,787],{"class":159},[142,1168,226],{"class":155},[142,1170,787],{"class":159},[142,1172,790],{"class":166},[142,1174,726],{"class":159},[142,1176,700],{"class":166},[142,1178,1180],{"class":144,"line":1179},59,[142,1181,1182],{"class":229},"    \"\"\"Configure root logging with redaction on every handler.\"\"\"\n",[142,1184,1186,1189,1191,1194],{"class":144,"line":1185},60,[142,1187,1188],{"class":166},"    handler ",[142,1190,258],{"class":155},[142,1192,1193],{"class":166}," logging.StreamHandler(stream)       ",[142,1195,1196],{"class":148},"# None means sys.stderr\n",[142,1198,1200],{"class":144,"line":1199},61,[142,1201,1202],{"class":166},"    handler.addFilter(RedactingFilter())\n",[142,1204,1206,1209,1211,1214,1217,1220,1223,1225],{"class":144,"line":1205},62,[142,1207,1208],{"class":166},"    handler.setFormatter(logging.Formatter(",[142,1210,273],{"class":229},[142,1212,1213],{"class":159},"%(levelname)s",[142,1215,1216],{"class":159}," %(name)s",[142,1218,1219],{"class":229},": ",[142,1221,1222],{"class":159},"%(message)s",[142,1224,273],{"class":229},[142,1226,1227],{"class":166},"))\n",[142,1229,1231,1234,1236],{"class":144,"line":1230},63,[142,1232,1233],{"class":166},"    root ",[142,1235,258],{"class":155},[142,1237,1238],{"class":166}," logging.getLogger()\n",[142,1240,1242,1245,1247],{"class":144,"line":1241},64,[142,1243,1244],{"class":166},"    root.handlers[:] ",[142,1246,258],{"class":155},[142,1248,1249],{"class":166}," [handler]\n",[142,1251,1253],{"class":144,"line":1252},65,[142,1254,1255],{"class":166},"    root.setLevel(level)\n",[10,1257,1258],{},"A few implementation details carry weight:",[34,1260,1261,1283,1293,1299],{},[37,1262,1263,1266,1267,1270,1271,1274,1275,1278,1279,1282],{},[68,1264,1265],{},"The message is formatted before masking"," (",[14,1268,1269],{},"record.getMessage()"," merges ",[14,1272,1273],{},"msg % args","), and ",[14,1276,1277],{},"args"," is cleared afterwards. Otherwise a secret passed as an argument — ",[14,1280,1281],{},"log.debug(\"token=%s\", token)"," — would be substituted after the filter ran.",[37,1284,1285,1288,1289,1292],{},[68,1286,1287],{},"Tracebacks are formatted and masked too."," A logged exception's text is rendered into ",[14,1290,1291],{},"exc_text"," and redacted, because exception messages routinely include URLs, connection strings and request details.",[37,1294,1295,1298],{},[68,1296,1297],{},"Longest values first."," If one secret contains another, replacing the longer first avoids leaving a partial secret behind.",[37,1300,1301,1304],{},[68,1302,1303],{},"A minimum length."," Registering a three-character value would mask ordinary words all over the output. Six characters is a practical floor for real credentials.",[10,1306,1307],{},"Registering secrets happens where they are read. With the resolver from the topic page, one line is enough:",[133,1309,1311],{"className":135,"code":1310,"language":137,"meta":138,"style":138},"from mytool.redact import redactor\n\n\ndef load_token(...):\n    token, source = resolve_token(...)\n    redactor.register(token.reveal())\n    return token, source\n",[14,1312,1313,1325,1329,1333,1343,1358,1363],{"__ignoreMap":138},[142,1314,1315,1317,1320,1322],{"class":144,"line":145},[142,1316,156],{"class":155},[142,1318,1319],{"class":166}," mytool.redact ",[142,1321,180],{"class":155},[142,1323,1324],{"class":166}," redactor\n",[142,1326,1327],{"class":144,"line":152},[142,1328,174],{"emptyLinePlaceholder":173},[142,1330,1331],{"class":144,"line":170},[142,1332,174],{"emptyLinePlaceholder":173},[142,1334,1335,1337,1340],{"class":144,"line":177},[142,1336,1142],{"class":155},[142,1338,1339],{"class":696}," load_token",[142,1341,1342],{"class":166},"(...):\n",[142,1344,1345,1348,1350,1353,1356],{"class":144,"line":186},[142,1346,1347],{"class":166},"    token, source ",[142,1349,258],{"class":155},[142,1351,1352],{"class":166}," resolve_token(",[142,1354,1355],{"class":159},"...",[142,1357,921],{"class":166},[142,1359,1360],{"class":144,"line":194},[142,1361,1362],{"class":166},"    redactor.register(token.reveal())\n",[142,1364,1365,1368],{"class":144,"line":202},[142,1366,1367],{"class":155},"    return",[142,1369,1370],{"class":166}," token, source\n",[1372,1373,1375],"h3",{"id":1374},"debug-flags-and-http-tracing","Debug flags and HTTP tracing",[10,1377,1378,1379,1381],{},"The riskiest moment is ",[14,1380,16],{},", because it usually turns on library logging you do not otherwise see — httpx and httpcore log request lines and, at trace level, headers. Wire the flag so redaction is already installed before those loggers are enabled:",[133,1383,1385],{"className":135,"code":1384,"language":137,"meta":138,"style":138},"import logging\n\nimport typer\n\nfrom mytool.redact import install\n\n\n@app.callback()\ndef main(debug: bool = typer.Option(False, \"--debug\", help=\"Verbose diagnostic output.\")) -> None:\n    install(logging.DEBUG if debug else logging.WARNING)\n    if debug:\n        logging.getLogger(\"httpx\").setLevel(logging.DEBUG)\n        logging.getLogger(\"httpcore\").setLevel(logging.DEBUG)\n",[14,1386,1387,1393,1397,1404,1408,1419,1423,1427,1434,1476,1499,1507,1522],{"__ignoreMap":138},[142,1388,1389,1391],{"class":144,"line":145},[142,1390,180],{"class":155},[142,1392,183],{"class":166},[142,1394,1395],{"class":144,"line":152},[142,1396,174],{"emptyLinePlaceholder":173},[142,1398,1399,1401],{"class":144,"line":170},[142,1400,180],{"class":155},[142,1402,1403],{"class":166}," typer\n",[142,1405,1406],{"class":144,"line":177},[142,1407,174],{"emptyLinePlaceholder":173},[142,1409,1410,1412,1414,1416],{"class":144,"line":186},[142,1411,156],{"class":155},[142,1413,1319],{"class":166},[142,1415,180],{"class":155},[142,1417,1418],{"class":166}," install\n",[142,1420,1421],{"class":144,"line":194},[142,1422,174],{"emptyLinePlaceholder":173},[142,1424,1425],{"class":144,"line":202},[142,1426,174],{"emptyLinePlaceholder":173},[142,1428,1429,1432],{"class":144,"line":215},[142,1430,1431],{"class":696},"@app.callback",[142,1433,750],{"class":166},[142,1435,1436,1438,1441,1444,1446,1448,1451,1454,1456,1459,1461,1464,1466,1469,1472,1474],{"class":144,"line":220},[142,1437,1142],{"class":155},[142,1439,1440],{"class":696}," main",[142,1442,1443],{"class":166},"(debug: ",[142,1445,1050],{"class":159},[142,1447,226],{"class":155},[142,1449,1450],{"class":166}," typer.Option(",[142,1452,1453],{"class":159},"False",[142,1455,910],{"class":166},[142,1457,1458],{"class":229},"\"--debug\"",[142,1460,910],{"class":166},[142,1462,1463],{"class":901},"help",[142,1465,258],{"class":155},[142,1467,1468],{"class":229},"\"Verbose diagnostic output.\"",[142,1470,1471],{"class":166},")) -> ",[142,1473,726],{"class":159},[142,1475,700],{"class":166},[142,1477,1478,1481,1484,1487,1490,1493,1495,1497],{"class":144,"line":233},[142,1479,1480],{"class":166},"    install(logging.",[142,1482,1483],{"class":159},"DEBUG",[142,1485,1486],{"class":155}," if",[142,1488,1489],{"class":166}," debug ",[142,1491,1492],{"class":155},"else",[142,1494,1156],{"class":166},[142,1496,1159],{"class":159},[142,1498,921],{"class":166},[142,1500,1501,1504],{"class":144,"line":238},[142,1502,1503],{"class":155},"    if",[142,1505,1506],{"class":166}," debug:\n",[142,1508,1509,1512,1515,1518,1520],{"class":144,"line":264},[142,1510,1511],{"class":166},"        logging.getLogger(",[142,1513,1514],{"class":229},"\"httpx\"",[142,1516,1517],{"class":166},").setLevel(logging.",[142,1519,1483],{"class":159},[142,1521,921],{"class":166},[142,1523,1524,1526,1529,1531,1533],{"class":144,"line":344},[142,1525,1511],{"class":166},[142,1527,1528],{"class":229},"\"httpcore\"",[142,1530,1517],{"class":166},[142,1532,1483],{"class":159},[142,1534,921],{"class":166},[81,1536],{"name":1537},"sec-debug-before-after",[1372,1539,1541],{"id":1540},"output-that-bypasses-logging","Output that bypasses logging",[10,1543,1544,1545,1547],{},"Not everything goes through ",[14,1546,42],{},". Three other paths need attention:",[34,1549,1550,1571,1589],{},[37,1551,1552,1555,1556,1559,1560,1563,1564,1566,1567,27],{},[68,1553,1554],{},"Uncaught tracebacks."," Python's default hook prints straight to stderr. Install a ",[14,1557,1558],{},"sys.excepthook"," that formats the traceback, passes it through ",[14,1561,1562],{},"redactor",", and prints it — or better, convert unexpected errors into a one-line message plus a redacted traceback only in ",[14,1565,16],{}," mode, as in ",[23,1568,1570],{"href":1569},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks\u002F","friendly error messages and tracebacks",[37,1572,1573,1576,1577,1580,1581,1584,1585,1588],{},[68,1574,1575],{},"Rich tracebacks with locals."," ",[14,1578,1579],{},"rich.traceback.install(show_locals=True)"," prints every local variable in every frame — including tokens. Leave ",[14,1582,1583],{},"show_locals"," off in anything users run, or wrap secrets in a type whose ",[14,1586,1587],{},"repr"," is masked so the locals display is harmless.",[37,1590,1591,1601,1602,1605,1606,1608],{},[68,1592,1593,1594,1597,1598,27],{},"Your own ",[14,1595,1596],{},"print"," and ",[14,1599,1600],{},"typer.echo"," Keep secrets wrapped in a masking type (see the ",[14,1603,1604],{},"Secret"," class in ",[23,1607,53],{"href":25},") and these become safe by default.",[29,1610,1612],{"id":1611},"ux-considerations","UX considerations",[34,1614,1615,1623,1636,1645,1654],{},[37,1616,1617,1576,1620,1622],{},[68,1618,1619],{},"Mask visibly.",[14,1621,100],{}," makes it obvious to the reader that something was removed on purpose, which is better than silently dropping text and leaving a confusing line.",[37,1624,1625,1628,1629,910,1632,1635],{},[68,1626,1627],{},"Keep enough context to debug."," Masking only the secret — ",[14,1630,1631],{},"Bearer ****",[14,1633,1634],{},"https:\u002F\u002F****@registry"," — leaves the header name and host visible, which is usually what a maintainer needs.",[37,1637,1638,1641,1642,1644],{},[68,1639,1640],{},"Tell users debug output is safe to share."," If you redact, say so in the help text for ",[14,1643,16],{},". It makes people more willing to provide diagnostics.",[37,1646,1647,1650,1651,27],{},[68,1648,1649],{},"Do not rely on redaction alone."," It is a safety net for mistakes, not a licence to log secrets. Code review should still reject ",[14,1652,1653],{},"log.debug(f\"token={token}\")",[37,1655,1656,1659,1660,1664],{},[68,1657,1658],{},"Redact files too."," If your tool writes a log file (see ",[23,1661,1663],{"href":1662},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fwriting-rotating-log-files-from-a-cli\u002F","writing rotating log files from a CLI","), attach the same filter to that handler.",[29,1666,1668],{"id":1667},"testing-the-behaviour","Testing the behaviour",[10,1670,1671],{},"Leak tests are cheap and high-value. Log through the real configuration and assert the secret never appears in captured output:",[133,1673,1675],{"className":135,"code":1674,"language":137,"meta":138,"style":138},"# tests\u002Ftest_redact.py\nimport io\nimport logging\n\nimport pytest\n\nfrom mytool.redact import MASK, Redactor, install, redactor\n\n\n@pytest.fixture\ndef captured():\n    stream = io.StringIO()\n    install(logging.DEBUG, stream=stream)   # the real configuration, into a buffer\n    return stream.getvalue\n\n\ndef test_registered_value_is_masked_everywhere(captured):\n    redactor.register(\"sk_live_51HxSecretValue\")\n    log = logging.getLogger(\"some.library\")\n    log.debug(\"connecting with key=%s to %s\", \"sk_live_51HxSecretValue\", \"api\")\n    out = captured()\n    assert \"sk_live_51HxSecretValue\" not in out and MASK in out\n\n\n@pytest.mark.parametrize(\"line, leaked\", [\n    (\"Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhIn0.sig\", \"eyJhbGci\"),\n    (\"GET https:\u002F\u002Fdeploy:hunter2@registry.example.com\u002Fv2\u002F\", \"hunter2\"),\n    (\"using ghp_16C7e42F292c6912E7710c838347Ae178B4a\", \"16C7e42F\"),\n    (\"password=correct-horse-battery\", \"correct-horse\"),\n])\ndef test_patterns(captured, line, leaked):\n    logging.getLogger(\"x\").warning(line)\n    assert leaked not in captured()\n\n\ndef test_tracebacks_are_redacted(captured):\n    redactor.register(\"tok_abcdefghijkl\")\n    try:\n        raise RuntimeError(\"request failed for token tok_abcdefghijkl\")\n    except RuntimeError:\n        logging.getLogger(\"x\").exception(\"boom\")\n    assert \"tok_abcdefghijkl\" not in captured()\n\n\ndef test_short_values_are_not_registered():\n    r = Redactor()\n    r.register(\"abc\")\n    assert r(\"abc def\") == \"abc def\"\n",[14,1676,1677,1682,1689,1695,1699,1706,1710,1723,1727,1731,1736,1746,1756,1775,1782,1786,1790,1800,1810,1825,1854,1864,1889,1893,1897,1910,1925,1939,1953,1967,1972,1982,1993,2007,2011,2015,2024,2033,2040,2055,2064,2078,2091,2095,2099,2108,2117,2127],{"__ignoreMap":138},[142,1678,1679],{"class":144,"line":145},[142,1680,1681],{"class":148},"# tests\u002Ftest_redact.py\n",[142,1683,1684,1686],{"class":144,"line":152},[142,1685,180],{"class":155},[142,1687,1688],{"class":166}," io\n",[142,1690,1691,1693],{"class":144,"line":170},[142,1692,180],{"class":155},[142,1694,183],{"class":166},[142,1696,1697],{"class":144,"line":177},[142,1698,174],{"emptyLinePlaceholder":173},[142,1700,1701,1703],{"class":144,"line":186},[142,1702,180],{"class":155},[142,1704,1705],{"class":166}," pytest\n",[142,1707,1708],{"class":144,"line":194},[142,1709,174],{"emptyLinePlaceholder":173},[142,1711,1712,1714,1716,1718,1720],{"class":144,"line":202},[142,1713,156],{"class":155},[142,1715,1319],{"class":166},[142,1717,180],{"class":155},[142,1719,338],{"class":159},[142,1721,1722],{"class":166},", Redactor, install, redactor\n",[142,1724,1725],{"class":144,"line":215},[142,1726,174],{"emptyLinePlaceholder":173},[142,1728,1729],{"class":144,"line":220},[142,1730,174],{"emptyLinePlaceholder":173},[142,1732,1733],{"class":144,"line":233},[142,1734,1735],{"class":696},"@pytest.fixture\n",[142,1737,1738,1740,1743],{"class":144,"line":238},[142,1739,1142],{"class":155},[142,1741,1742],{"class":696}," captured",[142,1744,1745],{"class":166},"():\n",[142,1747,1748,1751,1753],{"class":144,"line":264},[142,1749,1750],{"class":166},"    stream ",[142,1752,258],{"class":155},[142,1754,1755],{"class":166}," io.StringIO()\n",[142,1757,1758,1760,1762,1764,1767,1769,1772],{"class":144,"line":344},[142,1759,1480],{"class":166},[142,1761,1483],{"class":159},[142,1763,910],{"class":166},[142,1765,1766],{"class":901},"stream",[142,1768,258],{"class":155},[142,1770,1771],{"class":166},"stream)   ",[142,1773,1774],{"class":148},"# the real configuration, into a buffer\n",[142,1776,1777,1779],{"class":144,"line":392},[142,1778,1367],{"class":155},[142,1780,1781],{"class":166}," stream.getvalue\n",[142,1783,1784],{"class":144,"line":456},[142,1785,174],{"emptyLinePlaceholder":173},[142,1787,1788],{"class":144,"line":522},[142,1789,174],{"emptyLinePlaceholder":173},[142,1791,1792,1794,1797],{"class":144,"line":567},[142,1793,1142],{"class":155},[142,1795,1796],{"class":696}," test_registered_value_is_masked_everywhere",[142,1798,1799],{"class":166},"(captured):\n",[142,1801,1802,1805,1808],{"class":144,"line":602},[142,1803,1804],{"class":166},"    redactor.register(",[142,1806,1807],{"class":229},"\"sk_live_51HxSecretValue\"",[142,1809,921],{"class":166},[142,1811,1812,1815,1817,1820,1823],{"class":144,"line":674},[142,1813,1814],{"class":166},"    log ",[142,1816,258],{"class":155},[142,1818,1819],{"class":166}," logging.getLogger(",[142,1821,1822],{"class":229},"\"some.library\"",[142,1824,921],{"class":166},[142,1826,1827,1830,1833,1836,1839,1841,1843,1845,1847,1849,1852],{"class":144,"line":680},[142,1828,1829],{"class":166},"    log.debug(",[142,1831,1832],{"class":229},"\"connecting with key=",[142,1834,1835],{"class":159},"%s",[142,1837,1838],{"class":229}," to ",[142,1840,1835],{"class":159},[142,1842,273],{"class":229},[142,1844,910],{"class":166},[142,1846,1807],{"class":229},[142,1848,910],{"class":166},[142,1850,1851],{"class":229},"\"api\"",[142,1853,921],{"class":166},[142,1855,1856,1859,1861],{"class":144,"line":685},[142,1857,1858],{"class":166},"    out ",[142,1860,258],{"class":155},[142,1862,1863],{"class":166}," captured()\n",[142,1865,1866,1869,1872,1874,1877,1880,1882,1884,1886],{"class":144,"line":690},[142,1867,1868],{"class":155},"    assert",[142,1870,1871],{"class":229}," \"sk_live_51HxSecretValue\"",[142,1873,1087],{"class":155},[142,1875,1876],{"class":155}," in",[142,1878,1879],{"class":166}," out ",[142,1881,806],{"class":155},[142,1883,338],{"class":159},[142,1885,1876],{"class":155},[142,1887,1888],{"class":166}," out\n",[142,1890,1891],{"class":144,"line":703},[142,1892,174],{"emptyLinePlaceholder":173},[142,1894,1895],{"class":144,"line":709},[142,1896,174],{"emptyLinePlaceholder":173},[142,1898,1899,1902,1904,1907],{"class":144,"line":714},[142,1900,1901],{"class":696},"@pytest.mark.parametrize",[142,1903,279],{"class":166},[142,1905,1906],{"class":229},"\"line, leaked\"",[142,1908,1909],{"class":166},", [\n",[142,1911,1912,1915,1918,1920,1923],{"class":144,"line":731},[142,1913,1914],{"class":166},"    (",[142,1916,1917],{"class":229},"\"Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhIn0.sig\"",[142,1919,910],{"class":166},[142,1921,1922],{"class":229},"\"eyJhbGci\"",[142,1924,341],{"class":166},[142,1926,1927,1929,1932,1934,1937],{"class":144,"line":753},[142,1928,1914],{"class":166},[142,1930,1931],{"class":229},"\"GET https:\u002F\u002Fdeploy:hunter2@registry.example.com\u002Fv2\u002F\"",[142,1933,910],{"class":166},[142,1935,1936],{"class":229},"\"hunter2\"",[142,1938,341],{"class":166},[142,1940,1941,1943,1946,1948,1951],{"class":144,"line":766},[142,1942,1914],{"class":166},[142,1944,1945],{"class":229},"\"using ghp_16C7e42F292c6912E7710c838347Ae178B4a\"",[142,1947,910],{"class":166},[142,1949,1950],{"class":229},"\"16C7e42F\"",[142,1952,341],{"class":166},[142,1954,1955,1957,1960,1962,1965],{"class":144,"line":771},[142,1956,1914],{"class":166},[142,1958,1959],{"class":229},"\"password=correct-horse-battery\"",[142,1961,910],{"class":166},[142,1963,1964],{"class":229},"\"correct-horse\"",[142,1966,341],{"class":166},[142,1968,1969],{"class":144,"line":797},[142,1970,1971],{"class":166},"])\n",[142,1973,1974,1976,1979],{"class":144,"line":827},[142,1975,1142],{"class":155},[142,1977,1978],{"class":696}," test_patterns",[142,1980,1981],{"class":166},"(captured, line, leaked):\n",[142,1983,1984,1987,1990],{"class":144,"line":839},[142,1985,1986],{"class":166},"    logging.getLogger(",[142,1988,1989],{"class":229},"\"x\"",[142,1991,1992],{"class":166},").warning(line)\n",[142,1994,1995,1997,2000,2003,2005],{"class":144,"line":848},[142,1996,1868],{"class":155},[142,1998,1999],{"class":166}," leaked ",[142,2001,2002],{"class":155},"not",[142,2004,1876],{"class":155},[142,2006,1863],{"class":166},[142,2008,2009],{"class":144,"line":853},[142,2010,174],{"emptyLinePlaceholder":173},[142,2012,2013],{"class":144,"line":872},[142,2014,174],{"emptyLinePlaceholder":173},[142,2016,2017,2019,2022],{"class":144,"line":882},[142,2018,1142],{"class":155},[142,2020,2021],{"class":696}," test_tracebacks_are_redacted",[142,2023,1799],{"class":166},[142,2025,2026,2028,2031],{"class":144,"line":924},[142,2027,1804],{"class":166},[142,2029,2030],{"class":229},"\"tok_abcdefghijkl\"",[142,2032,921],{"class":166},[142,2034,2035,2038],{"class":144,"line":939},[142,2036,2037],{"class":155},"    try",[142,2039,700],{"class":166},[142,2041,2042,2045,2048,2050,2053],{"class":144,"line":954},[142,2043,2044],{"class":155},"        raise",[142,2046,2047],{"class":159}," RuntimeError",[142,2049,279],{"class":166},[142,2051,2052],{"class":229},"\"request failed for token tok_abcdefghijkl\"",[142,2054,921],{"class":166},[142,2056,2057,2060,2062],{"class":144,"line":969},[142,2058,2059],{"class":155},"    except",[142,2061,2047],{"class":159},[142,2063,700],{"class":166},[142,2065,2066,2068,2070,2073,2076],{"class":144,"line":979},[142,2067,1511],{"class":166},[142,2069,1989],{"class":229},[142,2071,2072],{"class":166},").exception(",[142,2074,2075],{"class":229},"\"boom\"",[142,2077,921],{"class":166},[142,2079,2080,2082,2085,2087,2089],{"class":144,"line":988},[142,2081,1868],{"class":155},[142,2083,2084],{"class":229}," \"tok_abcdefghijkl\"",[142,2086,1087],{"class":155},[142,2088,1876],{"class":155},[142,2090,1863],{"class":166},[142,2092,2093],{"class":144,"line":993},[142,2094,174],{"emptyLinePlaceholder":173},[142,2096,2097],{"class":144,"line":998},[142,2098,174],{"emptyLinePlaceholder":173},[142,2100,2101,2103,2106],{"class":144,"line":1009},[142,2102,1142],{"class":155},[142,2104,2105],{"class":696}," test_short_values_are_not_registered",[142,2107,1745],{"class":166},[142,2109,2110,2113,2115],{"class":144,"line":1014},[142,2111,2112],{"class":166},"    r ",[142,2114,258],{"class":155},[142,2116,1006],{"class":166},[142,2118,2119,2122,2125],{"class":144,"line":1019},[142,2120,2121],{"class":166},"    r.register(",[142,2123,2124],{"class":229},"\"abc\"",[142,2126,921],{"class":166},[142,2128,2129,2131,2134,2137,2140,2143],{"class":144,"line":1039},[142,2130,1868],{"class":155},[142,2132,2133],{"class":166}," r(",[142,2135,2136],{"class":229},"\"abc def\"",[142,2138,2139],{"class":166},") ",[142,2141,2142],{"class":155},"==",[142,2144,2145],{"class":229}," \"abc def\"\n",[10,2147,2148],{},"Keep a fixture of realistic secret shapes in the test suite and add to it whenever a new kind of credential enters the tool. The parametrised pattern test is the place for that.",[29,2150,2152],{"id":2151},"conclusion","Conclusion",[10,2154,2155,2156,2158,2159,2161],{},"Redaction turns \"please be careful what you log\" into a property of the system. Register every secret with a redactor when you read it, attach a filter to every logging handler that masks registered values and common token patterns — including in formatted tracebacks — and install it before ",[14,2157,16],{}," enables noisy library loggers. Close the other paths with a masking ",[14,2160,1604],{}," type and a redacting exception hook, and keep a parametrised leak test so it stays fixed.",[29,2163,2165],{"id":2164},"frequently-asked-questions","Frequently asked questions",[1372,2167,2169],{"id":2168},"does-redaction-slow-logging-down","Does redaction slow logging down?",[10,2171,2172],{},"A few string replacements and regular expressions per record cost microseconds, which is irrelevant for a CLI. If you log thousands of lines per second at debug level, precompile patterns (as above) and keep the registered-value set small.",[1372,2174,2176],{"id":2175},"should-i-redact-structured-json-logs-differently","Should I redact structured (JSON) logs differently?",[10,2178,2179,2180,910,2182,910,2184,2187],{},"Apply the same redactor to the rendered JSON string, or walk the event dictionary and redact string values and known key names (",[14,2181,616],{},[14,2183,306],{},[14,2185,2186],{},"authorization","). With structlog, a processor near the end of the chain is the natural place.",[1372,2189,2191,2192,2195],{"id":2190},"can-i-redact-values-in-json-command-output","Can I redact values in ",[14,2193,2194],{},"--json"," command output?",[10,2197,2198],{},"Command output is data the user asked for, so you normally should not alter it — but you should never put secrets in it in the first place. If an API returns a secret field, drop or mask it in your typed model before rendering.",[1372,2200,2202],{"id":2201},"what-should-i-do-if-a-token-has-already-leaked-into-a-log","What should I do if a token has already leaked into a log?",[10,2204,2205,2206,54,2209,2212],{},"Revoke it first and clean up second. Deleting the log line or editing the public ticket does not help once someone could have copied it, and many systems keep history and email notifications. Rotate the credential at the provider, then remove the text, then add the leaked shape to your parametrised redaction test so the same path cannot leak the next token. Secret scanners such as ",[14,2207,2208],{},"gitleaks",[14,2210,2211],{},"trufflehog"," are worth running over log archives and CI artefacts to find older exposures.",[1372,2214,2216,2217,2220],{"id":2215},"what-about-secrets-in-shell-completion-or-help","What about secrets in shell completion or ",[14,2218,2219],{},"--help","?",[10,2222,2223,2224,2227,2228,2231],{},"Never include a current value in completion candidates or help defaults. Typer shows ",[14,2225,2226],{},"envvar"," names in help, which is fine; configure ",[14,2229,2230],{},"show_default=False"," on any option whose default might be sensitive.",[29,2233,2235],{"id":2234},"related","Related",[34,2237,2238,2244,2249,2255,2261],{},[37,2239,2240,2241],{},"Up: ",[23,2242,2243],{"href":25},"Secrets and credentials in Python CLIs",[37,2245,2246],{},[23,2247,2248],{"href":57},"Reading secrets from env and files",[37,2250,2251],{},[23,2252,2254],{"href":2253},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis\u002F","Structured JSON logging in Python CLIs",[37,2256,2257],{},[23,2258,2260],{"href":2259},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-trace-ids-and-context-to-cli-logs\u002F","Adding trace IDs and context to CLI logs",[37,2262,2263],{},[23,2264,2265],{"href":1569},"Friendly error messages and tracebacks",[2267,2268,2269],"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 .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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":138,"searchDepth":152,"depth":152,"links":2271},[2272,2273,2274,2278,2279,2280,2281,2290],{"id":31,"depth":152,"text":32},{"id":62,"depth":152,"text":63},{"id":130,"depth":152,"text":131,"children":2275},[2276,2277],{"id":1374,"depth":170,"text":1375},{"id":1540,"depth":170,"text":1541},{"id":1611,"depth":152,"text":1612},{"id":1667,"depth":152,"text":1668},{"id":2151,"depth":152,"text":2152},{"id":2164,"depth":152,"text":2165,"children":2282},[2283,2284,2285,2287,2288],{"id":2168,"depth":170,"text":2169},{"id":2175,"depth":170,"text":2176},{"id":2190,"depth":170,"text":2286},"Can I redact values in --json command output?",{"id":2201,"depth":170,"text":2202},{"id":2215,"depth":170,"text":2289},"What about secrets in shell completion or --help?",{"id":2234,"depth":152,"text":2235},"2026-09-18","Stop tokens leaking through a Python CLI’s debug logs, HTTP traces and tracebacks: a redacting logging filter, known-value and pattern masking, and leak tests.","intermediate",false,"md",{},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fredacting-secrets-from-cli-output-and-logs",{"title":5,"description":2292},"cli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fredacting-secrets-from-cli-output-and-logs\u002Findex",[2301,42,2302,2303],"security","redaction","debugging","ZgRJGb3ip6crbmhpEtBWJMKcD69IoMkcbdH-TFhmpIo",[2306,2309,2312,2315,2318,2321,2324,2327,2330,2333,2336,2339,2342,2345,2348,2351,2354,2357,2360,2363,2366,2369,2372,2375,2378,2381,2384,2387,2390,2393,2396,2399,2402,2405,2408,2411,2414,2417,2420,2423,2426,2429,2432,2435,2438,2441,2444,2447,2450,2453,2456,2459,2462,2465,2468,2471,2474,2477,2480,2483,2486,2489,2492,2495,2498,2501,2504,2507,2510,2513,2516,2519,2522,2525,2528,2531,2534,2537,2540,2543,2546,2549,2552,2555,2558,2561,2564,2567,2570,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,2703,2706,2709,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],{"path":2307,"title":2308},"\u002Fabout","About Python CLI Toolcraft",{"path":2310,"title":2311},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies","Advanced Argument Validation Strategies",{"path":2313,"title":2314},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fparsing-nested-json-arguments-in-python-clis","Parsing Nested JSON Args in Python CLIs",{"path":2316,"title":2317},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-dependent-and-conflicting-options","Validating Dependent and Conflicting CLI Options",{"path":2319,"title":2320},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fvalidating-file-and-directory-paths-in-clis","Validating File and Directory Paths in CLIs",{"path":2322,"title":2323},"\u002Fadvanced-input-parsing-user-experience\u002Fadvanced-argument-validation-strategies\u002Fwriting-custom-click-parameter-types","Writing Custom Click Parameter Types",{"path":2325,"title":2326},"\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":2328,"title":2329},"\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":2331,"title":2332},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual","Building Terminal UIs with Textual for Python CLIs",{"path":2334,"title":2335},"\u002Fadvanced-input-parsing-user-experience\u002Fbuilding-terminal-uis-with-textual\u002Ftesting-textual-apps-with-pilot","Testing Textual Apps with Pilot",{"path":2337,"title":2338},"\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":2340,"title":2341},"\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":2343,"title":2344},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation","CLI Help Output and Documentation",{"path":2346,"title":2347},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fversioning-and-deprecating-cli-flags","Versioning and Deprecating CLI Flags",{"path":2349,"title":2350},"\u002Fadvanced-input-parsing-user-experience\u002Fcli-help-output-and-documentation\u002Fwriting-help-text-users-actually-read","Writing Help Text Users Actually Read",{"path":2352,"title":2353},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fadapting-output-to-terminal-width","Adapting Python CLI Output to Terminal Width",{"path":2355,"title":2356},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility\u002Fdetecting-ci-environments-and-non-interactive-shells","Detecting CI Environments and Non-Interactive Shells",{"path":2358,"title":2359},"\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":2361,"title":2362},"\u002Fadvanced-input-parsing-user-experience\u002Fcross-platform-terminal-compatibility","Cross-Platform Terminal Compatibility for Python CLIs",{"path":2364,"title":2365},"\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":2367,"title":2368},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fchoosing-exit-codes-for-cli-tools","Choosing Exit Codes for CLI Tools",{"path":2370,"title":2371},"\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":2373,"title":2374},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Ffriendly-error-messages-and-tracebacks","Friendly Error Messages and Tracebacks",{"path":2376,"title":2377},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes\u002Fhandling-keyboard-interrupt-cleanly","Handling Keyboard Interrupt Cleanly",{"path":2379,"title":2380},"\u002Fadvanced-input-parsing-user-experience\u002Ferror-handling-and-exit-codes","Error Handling and Exit Codes for CLIs",{"path":2382,"title":2383},"\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":2385,"title":2386},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Fconfig-precedence-flags-env-files-defaults","Config Precedence: Flags, Env, Files, Defaults",{"path":2388,"title":2389},"\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":2391,"title":2392},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars","Handling Config Files and Env Vars in CLIs",{"path":2394,"title":2395},"\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":2397,"title":2398},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Freading-toml-config-with-tomllib","Reading TOML Config with tomllib in Python CLIs",{"path":2400,"title":2401},"\u002Fadvanced-input-parsing-user-experience\u002Fhandling-configuration-files-env-vars\u002Ftyped-settings-with-pydantic-settings","Typed Settings with pydantic-settings in Python CLIs",{"path":2403,"title":2404},"\u002Fadvanced-input-parsing-user-experience","Advanced Input Parsing for Python CLIs",{"path":2406,"title":2407},"\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":2409,"title":2410},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Fbuilding-interactive-prompts-and-menus","Building Interactive Prompts and Menus in Python CLIs",{"path":2412,"title":2413},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich","Interactive Terminal UI with Rich",{"path":2415,"title":2416},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Flive-dashboards-with-rich-live","Live Dashboards with Rich Live in Python CLIs",{"path":2418,"title":2419},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Frendering-tables-and-json-with-rich","Rendering Tables and JSON with Rich",{"path":2421,"title":2422},"\u002Fadvanced-input-parsing-user-experience\u002Finteractive-terminal-ui-with-rich\u002Ftheming-rich-output-consistently","Theming Rich Output Consistently in Python CLIs",{"path":2424,"title":2425},"\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":2427,"title":2428},"\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":2430,"title":2431},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis","Shell Completion for Python CLIs",{"path":2433,"title":2434},"\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":2436,"title":2437},"\u002Fadvanced-input-parsing-user-experience\u002Fshell-completion-for-python-clis\u002Ftesting-shell-completion-in-python-clis","Testing Shell Completion in Python CLIs",{"path":2439,"title":2440},"\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":2442,"title":2443},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fadding-verbose-and-quiet-logging-flags","Adding Verbose and Quiet Logging Flags",{"path":2445,"title":2446},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps","Structured Logging for CLI Apps",{"path":2448,"title":2449},"\u002Fadvanced-input-parsing-user-experience\u002Fstructured-logging-for-cli-apps\u002Fstructured-json-logging-in-python-clis","Structured JSON Logging in Python CLIs",{"path":2451,"title":2452},"\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":2454,"title":2455},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fdetecting-tty-and-adapting-output","Detecting a TTY and Adapting Output",{"path":2457,"title":2458},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Femitting-json-output-for-scripting","Emitting JSON Output for Scripting",{"path":2460,"title":2461},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Fhandling-broken-pipe-and-sigpipe","Handling Broken Pipe and SIGPIPE",{"path":2463,"title":2464},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes","Working with stdin, stdout and Pipes",{"path":2466,"title":2467},"\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":2469,"title":2470},"\u002Fadvanced-input-parsing-user-experience\u002Fworking-with-stdin-stdout-and-pipes\u002Freading-piped-input-in-python-clis","Reading Piped Input in Python CLIs",{"path":2472,"title":2473},"\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":2475,"title":2476},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fdownloading-files-with-progress-in-python","Downloading Files with Progress in Python CLIs",{"path":2478,"title":2479},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis","Calling HTTP APIs from Python CLIs",{"path":2481,"title":2482},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Foauth-device-flow-login-for-clis","OAuth Device Flow Login for Python CLIs",{"path":2484,"title":2485},"\u002Fcli-runtime-systems-integration\u002Fcalling-http-apis-from-python-clis\u002Fpaginating-api-results-in-a-cli","Paginating API Results in a Python CLI",{"path":2487,"title":2488},"\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":2490,"title":2491},"\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":2493,"title":2494},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis","Concurrency and Async in Python CLIs",{"path":2496,"title":2497},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fmultiprocessing-for-cpu-bound-cli-tasks","Multiprocessing for CPU-Bound CLI Tasks",{"path":2499,"title":2500},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Fparallelising-cli-work-with-thread-pools","Parallelising CLI Work with Thread Pools",{"path":2502,"title":2503},"\u002Fcli-runtime-systems-integration\u002Fconcurrency-and-async-in-python-clis\u002Frate-limiting-concurrent-requests-in-clis","Rate-Limiting Concurrent Requests in Python CLIs",{"path":2505,"title":2506},"\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":2508,"title":2509},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fcross-platform-paths-with-pathlib","Cross-Platform Paths with pathlib in CLIs",{"path":2511,"title":2512},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Ffile-locking-for-concurrent-cli-runs","File Locking for Concurrent CLI Runs in Python",{"path":2514,"title":2515},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes","Filesystem Paths and Atomic Writes for CLIs",{"path":2517,"title":2518},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fsafe-temporary-files-and-directories","Safe Temporary Files and Directories in CLIs",{"path":2520,"title":2521},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fstoring-app-data-with-platformdirs","Storing CLI App Data with platformdirs",{"path":2523,"title":2524},"\u002Fcli-runtime-systems-integration\u002Ffilesystem-paths-and-atomic-writes\u002Fwriting-files-atomically-in-python-clis","Writing Files Atomically in Python CLIs",{"path":2526,"title":2527},"\u002Fcli-runtime-systems-integration","CLI Runtime & Systems Integration for Python",{"path":2529,"title":2530},"\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":2532,"title":2533},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis\u002Fhandling-sigterm-and-graceful-shutdown","Handling SIGTERM and Graceful Shutdown in CLIs",{"path":2535,"title":2536},"\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":2538,"title":2539},"\u002Fcli-runtime-systems-integration\u002Flong-running-and-watch-mode-clis","Long-Running and Watch-Mode Python CLIs",{"path":2541,"title":2542},"\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":2544,"title":2545},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Favoiding-shell-injection-in-python-clis","Avoiding Shell Injection in Python CLIs",{"path":2547,"title":2548},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fcalling-external-commands-safely-with-subprocess","Calling External Commands Safely with subprocess",{"path":2550,"title":2551},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fhandling-subprocess-timeouts-and-exit-codes","Handling Subprocess Timeouts and Exit Codes",{"path":2553,"title":2554},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis","Running Subprocesses from Python CLIs",{"path":2556,"title":2557},"\u002Fcli-runtime-systems-integration\u002Frunning-subprocesses-from-python-clis\u002Fstreaming-subprocess-output-in-real-time","Streaming Subprocess Output in Real Time",{"path":2559,"title":2560},"\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":2562,"title":2563},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis","Secrets and Credentials in Python CLIs",{"path":2565,"title":2566},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fprompting-for-passwords-securely","Prompting for Passwords Securely in Python CLIs",{"path":2568,"title":2569},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Freading-secrets-from-env-and-files","Reading Secrets from Env Vars and Files in CLIs",{"path":2297,"title":5},{"path":2572,"title":2573},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fstoring-tokens-with-keyring","Storing CLI Tokens Securely with keyring",{"path":2575,"title":2576},"\u002Fcli-runtime-systems-integration\u002Fsecrets-and-credentials-in-python-clis\u002Fsupporting-multiple-profiles-and-accounts","Supporting Multiple Profiles and Accounts in CLIs",{"path":2578,"title":2579},"\u002F","Python CLI Toolcraft",{"path":2581,"title":2582},"\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":2584,"title":2585},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading","CLI Startup Performance and Lazy Loading",{"path":2587,"title":2588},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Flazy-loading-subcommands-for-faster-startup","Lazy Loading Subcommands for Faster Startup",{"path":2590,"title":2591},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Fprofiling-python-cli-startup-time","Profiling Python CLI Startup Time",{"path":2593,"title":2594},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcli-startup-performance-and-lazy-loading\u002Freducing-cli-dependency-weight","Reducing CLI Dependency Weight",{"path":2596,"title":2597},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-subparsers-for-subcommands","argparse Subparsers for Subcommands",{"path":2599,"title":2600},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fargparse-vs-click-vs-typer-comparison","argparse vs Click vs Typer Compared",{"path":2602,"title":2603},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse","Command-Line Parsing with argparse",{"path":2605,"title":2606},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmigrating-from-argparse-to-typer","Migrating from argparse to Typer",{"path":2608,"title":2609},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fmutually-exclusive-options-in-argparse","Mutually Exclusive Options in argparse",{"path":2611,"title":2612},"\u002Fmodern-python-cli-frameworks-architecture\u002Fcommand-line-parsing-with-argparse\u002Fwriting-custom-argparse-actions","Writing Custom argparse Actions in Python",{"path":2614,"title":2615},"\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":2617,"title":2618},"\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":2620,"title":2621},"\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":2623,"title":2624},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions","Designing CLI Interfaces and Conventions in Python",{"path":2626,"title":2627},"\u002Fmodern-python-cli-frameworks-architecture\u002Fdesigning-cli-interfaces-and-conventions\u002Fnaming-commands-and-flags-consistently","Naming Commands and Flags Consistently in Python CLIs",{"path":2629,"title":2630},"\u002Fmodern-python-cli-frameworks-architecture","Python CLI Frameworks and Architecture",{"path":2632,"title":2633},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fdiscovering-plugins-with-entry-points","Discovering Plugins with Entry Points in Python CLIs",{"path":2635,"title":2636},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fhook-based-plugins-with-pluggy","Hook-Based Plugins for Python CLIs with pluggy",{"path":2638,"title":2639},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis","Plugin Architectures for Extensible CLIs",{"path":2641,"title":2642},"\u002Fmodern-python-cli-frameworks-architecture\u002Fplugin-architectures-for-extensible-clis\u002Fversioning-a-plugin-api","Versioning a Plugin API for a Python CLI",{"path":2644,"title":2645},"\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":2647,"title":2648},"\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":2650,"title":2651},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fdependency-injection-patterns-for-cli-commands","Dependency Injection Patterns for CLI Commands",{"path":2653,"title":2654},"\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":2656,"title":2657},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis","Structuring Multi-Command Python CLIs",{"path":2659,"title":2660},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-common-options-across-commands","Sharing Common Options Across Python CLI Commands",{"path":2662,"title":2663},"\u002Fmodern-python-cli-frameworks-architecture\u002Fstructuring-multi-command-python-clis\u002Fsharing-state-with-click-context-objects","Sharing State with Click Context Objects",{"path":2665,"title":2666},"\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":2668,"title":2669},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications","Testing Python CLI Applications",{"path":2671,"title":2672},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fmeasuring-cli-test-coverage","Measuring CLI Test Coverage",{"path":2674,"title":2675},"\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":2677,"title":2678},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fproperty-based-testing-cli-arguments-with-hypothesis","Property-Based Testing CLI Arguments with Hypothesis",{"path":2680,"title":2681},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Fsnapshot-testing-cli-output","Snapshot Testing CLI Output",{"path":2683,"title":2684},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-click-commands-with-clirunner","Testing Click Commands with CliRunner",{"path":2686,"title":2687},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftesting-python-cli-applications\u002Ftesting-interactive-prompts-and-stdin","Testing Interactive Prompts and stdin",{"path":2689,"title":2690},"\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":2692,"title":2693},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fbuilding-dynamic-commands-in-click","Building Dynamic Commands in Click",{"path":2695,"title":2696},"\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":2698,"title":2699},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each","Typer vs Click: When to Use Each",{"path":2701,"title":2702},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Ftyper-callback-functions-explained","Typer callback functions explained",{"path":2704,"title":2705},"\u002Fmodern-python-cli-frameworks-architecture\u002Ftyper-vs-click-when-to-use-each\u002Fusing-annotated-options-in-typer","Using Annotated Options in Typer",{"path":2707,"title":2708},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fautomating-releases-from-git-tags","Automating Python CLI Releases from Git Tags",{"path":2710,"title":2711},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis\u002Fcaching-uv-dependencies-in-ci","Caching uv Dependencies in CI for Python CLIs",{"path":2713,"title":2714},"\u002Fproject-setup-dependency-management\u002Fci-cd-pipelines-for-python-clis","CI\u002FCD Pipelines for Python CLIs",{"path":2716,"title":2717},"\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":2719,"title":2720},"\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":2722,"title":2723},"\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":2725,"title":2726},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fbuilding-a-cookiecutter-template-for-typer-clis","Building a Cookiecutter Template for Typer CLIs",{"path":2728,"title":2729},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fcopier-vs-cookiecutter-for-cli-templates","Copier vs Cookiecutter for CLI Templates",{"path":2731,"title":2732},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter","CLI Project Scaffolding with Cookiecutter",{"path":2734,"title":2735},"\u002Fproject-setup-dependency-management\u002Fcli-project-scaffolding-with-cookiecutter\u002Fpost-generation-hooks-in-cli-templates","Post-Generation Hooks in Python CLI Templates",{"path":2737,"title":2738},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbuilding-cross-platform-release-binaries-in-ci","Building Cross-Platform Release Binaries in CI",{"path":2740,"title":2741},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fbundling-a-python-cli-with-pyinstaller","Bundling a Python CLI with PyInstaller",{"path":2743,"title":2744},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fhomebrew-and-scoop-packaging-for-python-clis","Homebrew and Scoop Packaging for Python CLIs",{"path":2746,"title":2747},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries","Distributing CLIs as Standalone Binaries",{"path":2749,"title":2750},"\u002Fproject-setup-dependency-management\u002Fdistributing-clis-as-standalone-binaries\u002Fnuitka-vs-pyinstaller-for-python-clis","Nuitka vs PyInstaller for Python CLI Binaries",{"path":2752,"title":2753},"\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":2755,"title":2756},"\u002Fproject-setup-dependency-management","Project Setup & Dependency Management",{"path":2758,"title":2759},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code\u002Fconfiguring-ruff-for-a-cli-project","Configuring Ruff for a Python CLI Project",{"path":2761,"title":2762},"\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":2764,"title":2765},"\u002Fproject-setup-dependency-management\u002Flinting-and-type-checking-cli-code","Linting and Type-Checking Python CLI Code",{"path":2767,"title":2768},"\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":2770,"title":2771},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fautomating-changelogs-with-conventional-commits","Automating Changelogs with Conventional Commits",{"path":2773,"title":2774},"\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":2776,"title":2777},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fexposing-version-info-and-build-metadata","Exposing Version Info and Build Metadata",{"path":2779,"title":2780},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs","Managing CLI Versioning & Changelogs",{"path":2782,"title":2783},"\u002Fproject-setup-dependency-management\u002Fmanaging-cli-versioning-changelogs\u002Fsemantic-versioning-policy-for-cli-tools","A Semantic Versioning Policy for CLI Tools",{"path":2785,"title":2786},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbuilding-wheels-and-sdists-for-python-clis","Building Wheels and sdists for Python CLIs",{"path":2788,"title":2789},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fbundling-data-files-with-importlib-resources","Bundling Data Files with importlib.resources in CLIs",{"path":2791,"title":2792},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution","Packaging Python CLIs for Distribution",{"path":2794,"title":2795},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Finstalling-and-distributing-clis-with-pipx","Installing and Distributing CLIs with pipx",{"path":2797,"title":2798},"\u002Fproject-setup-dependency-management\u002Fpackaging-python-clis-for-distribution\u002Fpublishing-a-python-cli-to-pypi","Publishing a Python CLI to PyPI",{"path":2800,"title":2801},"\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":2803,"title":2804},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development","Poetry Workflows for CLI Development",{"path":2806,"title":2807},"\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":2809,"title":2810},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-dependency-groups-for-cli-tooling","Poetry Dependency Groups for CLI Tooling",{"path":2812,"title":2813},"\u002Fproject-setup-dependency-management\u002Fpoetry-workflows-for-cli-development\u002Fpoetry-entry-points-and-scripts-for-clis","Poetry Entry Points and Scripts for CLIs",{"path":2815,"title":2816},"\u002Fproject-setup-dependency-management\u002Fpre-commit-hooks-for-cli-projects","Pre-commit Hooks for CLI Projects",{"path":2818,"title":2819},"\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":2821,"title":2822},"\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":2824,"title":2825},"\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":2827,"title":2828},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management","uv for Python CLI Dependency Management",{"path":2830,"title":2831},"\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":2833,"title":2834},"\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":2836,"title":2837},"\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":2839,"title":2840},"\u002Fproject-setup-dependency-management\u002Fuv-for-python-cli-dependency-management\u002Fuv-workspaces-for-multi-package-clis","uv Workspaces for Multi-Package Python CLIs",{"path":2842,"title":2843},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices","Python CLI Env Isolation Best Practices",{"path":2845,"title":2846},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fmanaging-virtual-environments-for-cross-platform-clis","Managing Python CLI Virtual Environments",{"path":2848,"title":2849},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fpinning-the-python-version-for-a-cli","Pinning the Python Version for a CLI",{"path":2851,"title":2852},"\u002Fproject-setup-dependency-management\u002Fvirtual-environments-isolation-best-practices\u002Fsupporting-multiple-python-versions-with-nox","Supporting Multiple Python Versions with nox",1789736905051]