[{"data":1,"prerenderedAt":902},["ShallowReactive",2],{"docs-nav":3,"docs-\u002Fdocs\u002Fsecurity":78},[4,9,13,16,21,26,30,34,38,42,46,50,53,58,62,66,69,74],{"path":5,"title":6,"navTitle":6,"group":7,"order":8},"\u002Fdocs\u002Fcommands","Other commands","Guide",999,{"path":10,"title":11,"navTitle":12,"group":7,"order":8},"\u002Fdocs\u002Fcompaction","Context and compaction","Context & compaction",{"path":14,"title":15,"navTitle":15,"group":7,"order":8},"\u002Fdocs\u002Fconfiguration","Configuration",{"path":17,"title":18,"navTitle":18,"group":19,"order":20},"\u002Fdocs\u002Fenvironment","Environment variables","Reference",11,{"path":22,"title":23,"navTitle":23,"group":24,"order":25},"\u002Fdocs\u002Fflags","Flag reference","CLI",8,{"path":27,"title":28,"navTitle":28,"group":7,"order":29},"\u002Fdocs","Introduction",1,{"path":31,"title":32,"navTitle":32,"group":7,"order":33},"\u002Fdocs\u002Finstallation","Installation",2,{"path":35,"title":36,"navTitle":36,"group":19,"order":37},"\u002Fdocs\u002Fproviders","Providers",10,{"path":39,"title":40,"navTitle":40,"group":7,"order":41},"\u002Fdocs\u002Fquickstart","Quickstart",3,{"path":43,"title":44,"navTitle":45,"group":7,"order":8},"\u002Fdocs\u002Freleases","Releases and upgrading","Releases",{"path":47,"title":48,"navTitle":49,"group":7,"order":8},"\u002Fdocs\u002Frun","The run command","Running an audit",{"path":51,"title":52,"navTitle":52,"group":7,"order":8},"\u002Fdocs\u002Fsecurity","Security model",{"path":54,"title":55,"navTitle":56,"group":24,"order":57},"\u002Fdocs\u002Fsessions","Sessions and forking","Sessions",6,{"path":59,"title":60,"navTitle":61,"group":7,"order":8},"\u002Fdocs\u002Fskills-roles","Skills and roles","Skills & roles",{"path":63,"title":64,"navTitle":64,"group":19,"order":65},"\u002Fdocs\u002Ftools","Tool reference",12,{"path":67,"title":68,"navTitle":68,"group":7,"order":8},"\u002Fdocs\u002Ftroubleshooting","Troubleshooting",{"path":70,"title":71,"navTitle":72,"group":24,"order":73},"\u002Fdocs\u002Ftui","The interactive TUI","Interactive TUI",9,{"path":75,"title":76,"navTitle":77,"group":7,"order":8},"\u002Fdocs\u002Fweb-eval","Dashboard and evaluation","Dashboard & evaluation",{"id":79,"title":52,"body":80,"description":892,"extension":893,"group":7,"meta":894,"navTitle":52,"navigation":895,"order":8,"path":51,"seo":896,"stem":900,"__hash__":901},"docs\u002Fdocs\u002Fsecurity.md",{"type":81,"value":82,"toc":874},"minimark",[83,87,91,96,103,115,121,125,128,138,146,153,156,190,194,197,200,206,216,220,229,276,287,292,298,305,308,314,320,331,337,344,348,359,362,372,376,415,428,436,445,504,507,556,561,578,582,585,682,688,691,717,725,729,735,781,786,790,793,804,808,811,865,869],[84,85,52],"h1",{"id":86},"security-model",[88,89,90],"p",{},"locac reads and executes code written by someone who may want to attack the machine reading it. That\nis not a hypothetical for a vulnerability-research tool. It is the normal case. Everything below\nfollows from one rule.",[92,93,95],"h2",{"id":94},"the-rule","The rule",[88,97,98,102],{},[99,100,101],"strong",{},"The target repository is untrusted input."," Its source, its comments, its README, its test\nfixtures, and anything its code prints at runtime are all treated as attacker-authored, including as\na prompt-injection channel.",[88,104,105,106,110,111,114],{},"Everything that steers the agent, from configuration and prompt overrides to skills, roles and key\nbindings, comes from ",[107,108,109],"code",{},"LOCAC_HOME"," (",[107,112,113],{},"~\u002F.locac"," by default) and from nowhere else. locac never reads instructions\nout of the code it is auditing.",[88,116,117,118,120],{},"The session database lives under ",[107,119,109],{}," too, never inside the target root, because sandboxed\ntarget code has write access to that root.",[92,122,124],{"id":123},"the-untrusted-fence","The untrusted fence",[88,126,127],{},"Every tool result carrying target-derived bytes is wrapped before it reaches the model:",[129,130,136],"pre",{"className":131,"code":133,"language":134,"meta":135},[132],"language-text","⟦untrusted-output 3f9a1c02⟧\n\u003Cthe tool's output>\n⟦\u002Funtrusted-output 3f9a1c02⟧\n","text","",[107,137,133],{"__ignoreMap":135},[88,139,140,141,145],{},"The nonce is derived from the session id, so it differs between runs and cannot be predicted from\nthe target's contents. Output that tries to close the fence and continue as instruction text closes\nthe ",[142,143,144],"em",{},"wrong"," fence.",[88,147,148,149,152],{},"Piped stdin gets the same treatment; see ",[150,151,49],"a",{"href":47},".",[88,154,155],{},"Two supporting rules make the fence worth having:",[157,158,159,176],"ul",{},[160,161,162,168,169,172,173,175],"li",{},[99,163,164,167],{},[107,165,166],{},"content"," is the model's only channel."," Tool results also carry an out-of-band ",[107,170,171],{},"details"," object\nused by the TUI and the dashboard; no provider serialises it into the request. Anything the model\nis meant to act on must be in ",[107,174,166],{},", where the fence applies.",[160,177,178,181,182,185,186,189],{},[99,179,180],{},"Results are budgeted."," A tool result is capped (8,000 tokens by default) and truncated with a\nhint to narrow the request with ",[107,183,184],{},"offset","\u002F",[107,187,188],{},"limit"," or a tighter glob, rather than letting a 600 KB file\npush the fence markers out of the context window.",[92,191,193],{"id":192},"path-confinement","Path confinement",[88,195,196],{},"Every filesystem tool routes through a path guard that resolves the real path, following symlinks,\nand checks that it is inside the target root.",[88,198,199],{},"For a path that does not exist yet (a file about to be written), the guard canonicalises the nearest\nexisting ancestor and re-appends the remaining tail, so a symlinked parent directory cannot be used\nto place a new file outside the root.",[129,201,204],{"className":202,"code":203,"language":134,"meta":135},[132],"Path escapes the target root \u002Fhome\u002Fyou\u002Ftarget: \u002Fetc\u002Fpasswd. Retry with a path inside it.\nPath could not be contained to the target root …\n",[107,205,203],{"__ignoreMap":135},[88,207,208,209,212,213,152],{},"Skill directories are the one exception, and a narrow one: the bundled and user skill directories are\nmounted as ",[99,210,211],{},"read-only"," extra roots. They are visible to reads so the agent can open a skill's body,\nand invisible to writes and to ",[107,214,215],{},"execute",[92,217,219],{"id":218},"the-os-sandbox","The OS sandbox",[88,221,222,225,226,228],{},[107,223,224],{},"bash"," and ",[107,227,215],{}," run the target's code. When an OS sandbox is available they run inside it:",[230,231,232,245],"table",{},[233,234,235],"thead",{},[236,237,238,242],"tr",{},[239,240,241],"th",{},"Resource",[239,243,244],{},"Policy",[246,247,248,260,268],"tbody",{},[236,249,250,254],{},[251,252,253],"td",{},"Network",[251,255,256,259],{},[99,257,258],{},"All"," egress blocked. No allowed domains",[236,261,262,265],{},[251,263,264],{},"Read",[251,266,267],{},"The target root",[236,269,270,273],{},[251,271,272],{},"Write",[251,274,275],{},"The target root, plus scratch directories",[88,277,278,279,282,283,286],{},"On Windows the scratch list is empty: the sandbox backend runs commands as a ",[142,280,281],{},"different"," user, so\nthat user's ",[107,284,285],{},"%TMP%"," is already its own, and ACL-walking the operator's temp directory blew the\nbackend's setup budget on every run.",[288,289,291],"h3",{"id":290},"the-verdict-is-measured-not-assumed","The verdict is measured, not assumed",[88,293,294,295,152],{},"locac does not ask whether a sandbox is installed. It runs a no-op command through the sandbox,\nagainst the root this run will confine, and requires ",[99,296,297],{},"exit 0 and the probe token back on stdout",[88,299,300,301,304],{},"That distinction is load-bearing. Ubuntu 24.04+ ships\n",[107,302,303],{},"kernel.apparmor_restrict_unprivileged_userns=1",", under which the dependency check passes, the outer\nbubblewrap starts, the nested user namespace is denied, and every command then exits 1 with no\noutput. A capability check reports a working sandbox; the probe reports the truth. The probe is\ncapped at 20 seconds, so a wedged sandbox degrades the run instead of stalling its startup.",[88,306,307],{},"Without a working sandbox the run still starts, unconfined, and says so:",[129,309,312],{"className":310,"code":311,"language":134,"meta":135},[132],"locac: sandbox=direct — NO OS confinement. Target code runs with cwd pinned to the root only.\n  why: \u003Cthe host's own reason>\n  use --require-sandbox to refuse to start unconfined.\n",[107,313,311],{"__ignoreMap":135},[88,315,316,319],{},[107,317,318],{},"--require-sandbox"," turns that notice into a refusal.",[88,321,322,323,326,327,330],{},"A sandbox can also be ",[142,324,325],{},"partial",". On Linux, confinement is two layers: bubblewrap for filesystem and\nnetwork, and a seccomp stage that blocks ",[107,328,329],{},"AF_UNIX"," so confined code cannot talk its way out over a\nhost socket. The second layer can go missing silently, and a compiled binary triggered exactly that,\nbecause the stage binary was resolved from the library's own module URL, which inside a compiled\nartifact is the embedded virtual filesystem. Measured on Ubuntu 26.04: the binary refused writes\noutside the root and had no DNS, yet a world-writable system bus socket stayed reachable from\n\"sandboxed\" code. locac now reads the wrapped argument vector itself and reports what is missing:",[129,332,335],{"className":333,"code":334,"language":134,"meta":135},[132],"locac: sandbox=os — unix-socket blocking off: no apply-seccomp, AF_UNIX egress is not confined.\n",[107,336,334],{"__ignoreMap":135},[88,338,339,340,343],{},"The verdict is host-dependent by nature, so it is printed for the operator and ",[99,341,342],{},"never"," reaches the\nmodel or the system prompt, which have to be byte-identical across machines.",[288,345,347],{"id":346},"confined-commands-serialize-across-roots","Confined commands serialize across roots",[88,349,350,351,354,355,358],{},"The sandbox manager's configuration, its network bridges, its cleanup, and on Windows its filesystem\nACEs are all ",[99,352,353],{},"process-global",". Two audits of two different repositories in one process - which the\ndashboard can start, and which ",[107,356,357],{},"eval ab"," does by design - therefore share one fence, and the second\nroot asking to be pinned used to move that fence while the first root's command was still running.",[88,360,361],{},"A process-wide execution lease now covers the whole command lifetime: pinning, wrapping, spawning,\nwaiting, and cleanup. The fence cannot be re-pointed underneath a live child. The cost is real -\nconfined commands from different roots run one at a time rather than in parallel - and it is the\nintended trade: a command that waits is slower, while a command whose confinement moved mid-flight is\nunconfined.",[88,363,364,365,367,368,371],{},"Per-OS prerequisites are in ",[150,366,32],{"href":31},"; ",[107,369,370],{},"locac selftest"," reports what this\nhost actually gets.",[92,373,375],{"id":374},"the-approval-gate","The approval gate",[230,377,378,388],{},[233,379,380],{},[236,381,382,385],{},[239,383,384],{},"Category",[239,386,387],{},"Default",[246,389,390,398,408],{},[236,391,392,395],{},[251,393,394],{},"Read-only tools",[251,396,397],{},"Allow",[236,399,400,403],{},[251,401,402],{},"Dangerous tools",[251,404,405],{},[99,406,407],{},"Ask",[236,409,410,413],{},[251,411,412],{},"Everything else",[251,414,397],{},[88,416,417,418,421,422,425,426,152],{},"\"Ask\" with no way to ask is ",[99,419,420],{},"denied",". A piped or ",[107,423,424],{},"--print"," run has no interactive terminal, so a\ndangerous call is blocked rather than waved through. The full resolution order and the exact\ndangerous set are in the ",[150,427,64],{"href":63},[288,429,431,432,435],{"id":430},"what-auto-will-and-will-not-approve","What ",[107,433,434],{},"--auto"," will and will not approve",[88,437,438,440,441,444],{},[107,439,434],{}," auto-approves a shell command only when ",[99,442,443],{},"all"," of the following hold:",[446,447,448,454,469,485],"ol",{},[160,449,450,451,152],{},"The leading token is in a fixed read-only list: ",[107,452,453],{},"ls cat head tail wc file stat grep egrep rg find fd tree pwd echo which basename dirname realpath sort cut nl strings sha256sum md5sum true",[160,455,456,457,460,461,464,465,468],{},"The command contains no shell metacharacter: ",[107,458,459],{},"; & | \\"," $ % > \u003C newline ( ) * ? { } ",[462,463],"span",{}," !",[107,466,467],{},", or a ","..` path segment.",[160,470,471,472,475,476,475,478,467,481,484],{},"No argument starts at a root: ",[107,473,474],{},"~",", ",[107,477,185],{},[107,479,480],{},"\\",[107,482,483],{},"C:","-style drive prefix.",[160,486,487,488,491,492,475,495,475,498,491,501,152],{},"No write or execute flag is present: ",[107,489,490],{},"find -delete\u002F-exec\u002F-execdir\u002F-ok\u002F-okdir\u002F-fls\u002F-fprint\u002F-fprintf",",\n",[107,493,494],{},"fd -x\u002F-X\u002F--exec\u002F--exec-batch",[107,496,497],{},"rg --pre\u002F--hostname-bin",[107,499,500],{},"sort -o\u002F--output",[107,502,503],{},"tree -o\u002F--output",[88,505,506],{},"Three details are worth knowing, because each of them was a bug first:",[157,508,509,523,540],{},[160,510,511,514,515,518,519,522],{},[99,512,513],{},"Quotes are stripped before inspection."," Reading the raw string let ",[107,516,517],{},"cat \"\u002Fetc\u002Fpasswd\""," and\n",[107,520,521],{},"find . \"-delete\""," through, because neither matched a pattern anchored on the unquoted form.",[160,524,525,531,532,535,536,539],{},[99,526,527,530],{},[107,528,529],{},"uniq"," is deliberately absent"," from the read-only list. ",[107,533,534],{},"uniq in out"," writes to a ",[142,537,538],{},"positional","\nargument, so it is not a reader.",[160,541,542,548,549,552,553,152],{},[99,543,544,547],{},[107,545,546],{},"%"," is a metacharacter"," because ",[107,550,551],{},"cmd \u002Fc"," expands ",[107,554,555],{},"%USERPROFILE%",[88,557,558,560],{},[107,559,215],{}," is never auto-approved, whatever the command looks like. The fallback for anything\nunclassified is deny.",[562,563,564],"blockquote",{},[88,565,566,567,570,571,573,574,577],{},"The classification is ",[99,568,569],{},"lexical",". A symlink inside the repository still resolves outside it, and\n",[107,572,434],{}," will not notice. It is a convenience layer over the OS sandbox, not a replacement for it,\nwhich is why ",[107,575,576],{},"--auto --require-sandbox"," is the combination worth typing.",[92,579,581],{"id":580},"the-dast-egress-guard","The DAST egress guard",[88,583,584],{},"The DAST tools send live HTTP requests, which makes them an SSRF primitive if left unguarded. The\nguard is deliberately permissive about private ranges and absolute about the rest.",[230,586,587,597],{},[233,588,589],{},[236,590,591,594],{},[239,592,593],{},"Destination",[239,595,596],{},"Result",[246,598,599,615,633,647,659,670],{},[236,600,601,612],{},[251,602,603,475,606,475,609],{},[107,604,605],{},"127.0.0.1",[107,607,608],{},"localhost",[107,610,611],{},"::1",[251,613,614],{},"Allowed. You are scanning your own lab",[236,616,617,630],{},[251,618,619,620,475,623,475,626,629],{},"RFC1918 (",[107,621,622],{},"10\u002F8",[107,624,625],{},"172.16\u002F12",[107,627,628],{},"192.168\u002F16",")",[251,631,632],{},"Allowed, same reason",[236,634,635,642],{},[251,636,637,638,641],{},"Any non-",[107,639,640],{},"http(s)"," scheme",[251,643,644],{},[99,645,646],{},"Blocked",[236,648,649,655],{},[251,650,651,654],{},[107,652,653],{},"169.254.0.0\u002F16"," (link-local \u002F cloud metadata)",[251,656,657],{},[99,658,646],{},[236,660,661,666],{},[251,662,663],{},[107,664,665],{},"fe80::\u002F10",[251,667,668],{},[99,669,646],{},[236,671,672,678],{},[251,673,674,677],{},[107,675,676],{},"fd00:ec2::254"," (IMDS over IPv6)",[251,679,680],{},[99,681,646],{},[129,683,686],{"className":684,"code":685,"language":134,"meta":135},[132],"scheme \"file\" is not allowed — DAST targets must be http(s) (blocks file:\u002F\u002F local read + data:\u002Fgopher: SSRF)\nlink-local \u002F cloud-metadata host \"169.254.169.254\" is blocked (SSRF to instance credentials)\nhost \"metadata.internal\" resolves to blocked address 169.254.169.254 (SSRF to link-local \u002F cloud-metadata)\n",[107,687,685],{"__ignoreMap":135},[88,689,690],{},"Two mechanics matter:",[157,692,693,707],{},[160,694,695,698,699,702,703,706],{},[99,696,697],{},"IPv4-mapped IPv6 is decoded."," The URL parser canonicalises ",[107,700,701],{},"::ffff:169.254.169.254"," to\n",[107,704,705],{},"::ffff:a9fe:a9fe",", which defeats a string comparison. The guard recovers the embedded IPv4 address\nand checks that.",[160,708,709,712,713,716],{},[99,710,711],{},"Hostnames are resolved, then checked"," on ",[142,714,715],{},"every"," egress, not once at configuration time. If\nany resolved address is blocked, the request is blocked.",[562,718,719],{},[88,720,721,724],{},[99,722,723],{},"Known residual: DNS rebinding."," This is resolve-then-check, not connect-time IP pinning, so a\nname that resolves benignly during the check and maliciously during the connect is not stopped.\nMitigating it are the facts that DAST is operator-initiated, permission-gated, and circuit-broken.\nIf that is not enough for your environment, do not enable the DAST tools.",[92,726,728],{"id":727},"the-dashboard","The dashboard",[88,730,731,734],{},[107,732,733],{},"locac web"," can launch runs, and a run executes bash. The dashboard is therefore treated as a local\nRCE surface.",[157,736,737,747,750,756,767,778],{},[160,738,739,740,742,743,746],{},"It binds ",[107,741,605],{}," by default. Binding anything else is ",[99,744,745],{},"refused"," unless a dashboard password\nand a JWT secret are both configured.",[160,748,749],{},"The password is stored as an argon2id hash, never in plaintext.",[160,751,752,753,152],{},"Sessions are stateless HS256 JWTs verified with a constant-time comparison; the cookie is\n",[107,754,755],{},"HttpOnly; SameSite=Strict; Path=\u002F",[160,757,758,759,762,763,766],{},"Every ",[107,760,761],{},"\u002Fapi\u002F"," route, reads included, is Host-guarded against DNS rebinding, and every mutation\nadditionally requires a same-origin ",[107,764,765],{},"Origin"," and a per-startup CSRF token header.",[160,768,769,770,773,774,777],{},"Run-launch routes are ",[99,771,772],{},"off"," unless ",[107,775,776],{},"--enable-runs"," is passed.",[160,779,780],{},"All untrusted content, from transcripts and tool output to finding evidence and live stream frames, is\nrendered as text by the client, never as HTML.",[88,782,783,784,152],{},"Details are in ",[150,785,76],{"href":75},[92,787,789],{"id":788},"secrets","Secrets",[88,791,792],{},"The API key is never echoed, never persisted outside the config file you wrote it into, never\nlogged, and never written into a finding, a transcript, or a memory entry. Anything the dashboard\nsends to the browser has it replaced with a redaction placeholder, and saving a form back preserves\nthe stored key rather than writing the placeholder over it.",[88,794,795,796,799,800,803],{},"The one place a key can leak is your own doing: ",[107,797,798],{},"locac config show"," prints the effective\nconfiguration, including a literal ",[107,801,802],{},"apiKey",". Do not paste that output into a bug report.",[92,805,807],{"id":806},"what-this-does-not-defend-against","What this does not defend against",[88,809,810],{},"Stated plainly, because a security tool that overstates its own guarantees is worse than one that\ndoes not try:",[157,812,813,822,828,836,842,855],{},[160,814,815,821],{},[99,816,817,820],{},[107,818,819],{},"sandbox=direct"," is not confinement."," The cwd is pinned; nothing else is. If the host has no\nworking sandbox, target code runs with your privileges.",[160,823,824,827],{},[99,825,826],{},"Inside the sandbox, the target root is writable",", and on POSIX so is the system temp\ndirectory. Do not audit a repository you are not prepared to see modified.",[160,829,830,835],{},[99,831,832,834],{},[107,833,434],{}," is lexical",", as described above.",[160,837,838,841],{},[99,839,840],{},"DNS rebinding against the DAST guard"," remains possible.",[160,843,844,854],{},[99,845,846,849,850,853],{},[107,847,848],{},"--yes"," \u002F ",[107,851,852],{},"--allow-all"," disables the gate entirely."," That is what they are for; know what you\nare pointing them at.",[160,856,857,860,861,864],{},[99,858,859],{},"A prompt-injection payload can still influence the model."," The fence and the invariants make it\nfar harder for target text to be ",[142,862,863],{},"obeyed",", but the defence is a strong prior, not a proof. The\nevidence gates are the backstop: a finding cannot claim High or Critical without an execution\nartifact whose checked assertion passed for that exact location, plus a passing quorum, whatever\nthe model was told.",[92,866,868],{"id":867},"next","Next",[88,870,871,873],{},[150,872,11],{"href":10}," covers how old tool output is elided without losing the\nevidence that a finding depends on.",{"title":135,"searchDepth":41,"depth":41,"links":875},[876,877,878,879,883,887,888,889,890,891],{"id":94,"depth":33,"text":95},{"id":123,"depth":33,"text":124},{"id":192,"depth":33,"text":193},{"id":218,"depth":33,"text":219,"children":880},[881,882],{"id":290,"depth":41,"text":291},{"id":346,"depth":41,"text":347},{"id":374,"depth":33,"text":375,"children":884},[885],{"id":430,"depth":41,"text":886},"What --auto will and will not approve",{"id":580,"depth":33,"text":581},{"id":727,"depth":33,"text":728},{"id":788,"depth":33,"text":789},{"id":806,"depth":33,"text":807},{"id":867,"depth":33,"text":868},"[object Object]","md",{},true,{"title":52,"description":897},{"How locac treats the repository under audit as attacker-authored input":898,"group":19,"order":899},"untrusted fences, path confinement, the OS sandbox, the approval gate, the DAST egress guard, and what it deliberately does not defend against.",13,"docs\u002Fsecurity","YmjctFUIkjex7k4Bj7k_vuVxzLqgtGvOADjmfEraC_Q",1786794387187]