Announcing Beacon: Self-improving memory for coding agents
← All skills
Skill

local-api

skill:smol-machines/smolvm#local-api

Drives smolvm programmatically over its local HTTP API (smolvm serve) instead of the CLI: create machines, exec and stream commands, move files in and out, and tear them down. Use when building a client, harness, agent tool or MCP backend over smolvm; when a create call is accepted but the machine behaves as if a field was ignored; when a file uploaded over the API has disappeared; when an exec that failed still returned HTTP 200; or when choosing between the Unix socket and loopback TCP. Do not use it as a substitute for the CLI in a shell script, and do not bind a plain listener beyond loopback: a server started without mutual TLS has no authentication of any kind.

Verdict
Suspicious

Risky behavior the analysis could not rule out.

Analyzed Oct 27, 2026, 23:07 UTC at cd55588

Dispute security verdict

If you believe this verdict is incorrect, submit a dispute and we'll review it.

4
findings
critical
highest severity
3
threat classes
683
files scanned
343/372
checks satisfied
0
package advisories

Why this verdict

  • suspicious: finding skill:docs/local-api:crates/smolvm-agent/src/main.rs:572:CRED
  • suspicious: finding skill:docs/local-api:crates/smolvm-agent/src/main.rs:6876:EXFIL
  • suspicious: finding skill:docs/local-api:crates/smolvm-agent/src/main.rs:7386:CRED
  • suspicious: finding skill:docs/local-api:docs/local-api/scripts/cleanup.sh:161:DESTRUCT

Security findings 4

Data exfiltration

criticalContextual riskmedium confidence

The smolvm guest agent ships a TCP connectivity diagnostic that opens a raw socket to the public address 1.1.1.1:80 and sends an HTTP GET (with wget/nc/crane/curl fallbacks), an outbound network contact not declared by the skill, though the payload is a generic probe rather than workspace data.

(e.g., 
vector:code
Counter-argument considered

The strongest case against the finding is that the payload is a constant probe and no machine data leaves the host, but the claim already concedes exactly that and rests on the outbound contact itself: the executed guest agent hardcodes 1.1.1.1 (TcpStream, nc to '1.1.1.1 80', curl to http://1.1.1.1), the skill's declared claims never declare this external endpoint, and no carve-out or mitigation covers an undeclared hardcoded external contact in shipped code, so the finding stands as a contextual risk.

Credential access

highContextual risklow confidence

The smolvm guest agent shipped with this skill's repository starts an SSH-agent forwarding bridge that puts the host's SSH agent socket in reach of every guest child process when the host enables it — a credential-adjacent capability that the local-api skill's declared purpose (machine lifecycle, exec, and file transfer over the local HTTP API) does not mention, and it is undeclared in the evidence shown here (unexplained, plausibly an honest opt-in product feature).

Start SSH agent forwarding bridge if enabled by host
vector:code
Counter-argument considered

The shown boot-path code confirms the agent starts an SSH-agent forwarding bridge whenever the host enables it and exports the agent socket to every guest child process; no shown declared claim (D3/D41) or the skill description covers this credential-adjacent capability, and no carve-out or mitigation neutralizes it, so the contextual CRED finding stands.

Credential access

highContextual risklow confidence

The smolvm agent injects SSH_AUTH_SOCK into the environment of commands executed in guests via ssh_agent::inject_into_env, exposing a forwarded SSH agent (a host credential-signing surface) to workload processes when forwarding is enabled — a credential path not mentioned in the skill's declared machine-lifecycle description.

SSH agent forwarding: make SSH_AUTH_SOCK part of the command env so it
vector:code
Counter-argument considered

The shown code copies the command env, calls ssh_agent::inject_into_env, and passes that env to storage::run_command, so a forwarded host SSH agent does reach guest workload processes in the exec path the skill drives; the opt-in gating ('No-op when forwarding is off') and the absence of any declared claim about SSH forwarding bound severity but do not make the credential-path claim wrong.

Destructive action

highContextual riskmedium confidence

cleanup.sh's opt-in --reap mode runs kill -9 on every smolvm VM process found under the user's HOME (via /proc and ps scans), including machines created by other packets or sessions that this script never recorded.

With --reap every one is killed, including a machine another packet kept.
trigger:conditionalvector:code
Counter-argument considered

The claim matches the script verbatim: with --reap the loop SIGKILLs every pid fed from list_vm_processes, and the script itself states the blast radius; the mode is opt-in, scoped to the user's HOME and openly declared ('kill every VM process under this HOME; run without it first'), which lowers severity but does not make the destructive-behavior finding wrong.

Open questions 11

What the analysis could not settle from the code alone.

[file request crates/smolvm-agent/src/credentials.rs] The agent declares a credentials module: what credentials does it read or store (e.g. registry auth), and does any credential value leave the guest or reach a network destination beyond the registry being pulled from?
Credential access
[file request crates/smolvm-agent/src/ssh_agent.rs] How does the SSH agent forwarding bridge obtain the host's agent connection over vsock, where is the forwarded socket exposed in the guest, and is access to it restricted to any workload or user?
Credential access
[file request crates/smolvm-agent/src/publish_socket.rs] Which Unix sockets do the --expose-socket / --mount-socket bridges connect, in which direction (host to guest or guest to host), and is bridged access restricted or logged?
Credential access
[file request crates/smolvm-agent/src/credentials.rs] What does credentials::inject_into_container inject into the workload container's OCI spec, and from where do those credentials originate?
Credential access
[file request crates/smolvm-agent/src/ssh_agent.rs] What do ssh_agent::inject_into_env and ssh_agent::inject_into_container forward into exec/run environments and container specs, and from which host socket or path?
Credential access
[file request crates/smolvm-agent/src/credentials.rs] What does credentials::inject_into_container read (files, env, mounts) and copy or mount into the container OCI spec, and is any credential forwarding declared by the artifact?
Credential access
[file request docs/local-api/scripts/preflight.sh] What does preflight.sh actually execute — does it download or run remote code, read credentials or secret-bearing environment variables, or write outside the workspace, beyond reporting auth=none_unless_mtls?
Code execution
[file request crates/smolvm-agent/src/main.rs] The served block starts mid-function at the raw-socket test and its result JSON keys include 'rust_std_net': connect_result; where does the TCP-connect target for that first test come from (host-supplied nettest request URL versus a hardcoded 1.1.1.1 default), what triggers this nettest handler, and do the earlier tests (wget/crane or others) send anything beyond a bare probe request?
Reconnaissance
[file request crates/smolvm-agent/src/ssh_agent.rs] Does ssh_agent::inject_into_env inject SSH_AUTH_SOCK only when the operator explicitly enabled agent forwarding, and what socket path does the injected variable point to?
Credential access
[file request crates/smolvm-agent/src/main.rs] Which request handler dispatches to the network connectivity test (test_tcp_syscall and the nc/curl probes to 1.1.1.1), is it gated behind an explicit diagnostic request, and is it exposed through the documented local HTTP API surface?
Data exfiltration
[file request crates/smolvm-agent/src/ssh_agent.rs] Does ssh_agent::inject_into_env only inject SSH_AUTH_SOCK when the operator explicitly enabled agent forwarding, and what socket path does the injected variable point to?
Credential access
Refuted findings 2

Candidates the review stage examined and rejected, kept for transparency.

Code execution

criticalRefutedlow confidence

The skill's procedure instructs the agent to execute bundled shell scripts as prerequisite/setup steps (scripts/preflight.sh, scripts/serve-start.sh, scripts/lifecycle-check.sh, scripts/cleanup.sh --purge) whose contents are not present in the reviewed evidence, so what runs on the host cannot be verified from this artifact view.

scripts/preflight.sh
Why it was refuted

The strongest case against the finding was its unverifiability premise, and the follow-up served the script contents, breaking it: preflight.sh is read-only host checks with no writes, downloads, or binds; serve-start.sh launches smolvm on a default Unix socket under the state dir; lifecycle-check.sh only curls the local API over that socket or loopback — all the declared core capability, with cleanup.sh's behavior (delete recorded machines, stop server, remove socket) declared in the skill text and its benign --record invocation visible in lifecycle-check.sh. The strongest case for the finding (four opaque scripts on the host) is now three verified-benign files plus one whose declared teardown behavior matches the declared 'tear them down' capability, so the harm and undeclared gates fail and the finding dies.

Reconnaissance

mediumRefutedlow confidence

The VM agent's nettest exposes a TCP-connect primitive that connects to a parameter-named host:port and sends a bare HTTP GET (plus hardcoded 1.1.1.1:80 probes via nc and curl), which would let whoever can trigger the nettest probe arbitrary outbound hosts from the VM if the target is host-supplied.

match TcpStream::connect_timeout(&addr, Duration::from_secs(NETWORK_TEST_TIMEOUT_SECS)) {
Why it was refuted

Refuted on harm: the nettest is served over the agent's host-request surface, and its only shown caller — the host controller — already holds unrestricted command execution in the same VM (exec handlers shown), while any in-guest process can equally open outbound TCP natively, so the connect primitive crosses no boundary and grants no capability beyond the declared exec core; the hardcoded 1.1.1.1 curl/nc probes are connectivity canaries to a public resolver, not an attacker-controlled sink, and the cited D3 contradiction is spurious because D3 forbids binding a listener beyond loopback, not outbound connects.

Coverage gaps 50

Parts of the artifact the static analysis could not fully examine.

context:submodule_present submodules are not fetched
.gitmodules
context:lfs_pointer git-lfs object not fetched
lib/libMoltenVK.dylib
context:lfs_pointer git-lfs object not fetched
lib/libepoxy.0.dylib
context:lfs_pointer git-lfs object not fetched
lib/libkrun.dylib
context:lfs_pointer git-lfs object not fetched
lib/libkrunfw.5.dylib
context:lfs_pointer git-lfs object not fetched
lib/libvirglrenderer.1.dylib
context:lfs_pointer git-lfs object not fetched
lib/linux-aarch64/libkrun.so
context:lfs_pointer git-lfs object not fetched
lib/linux-aarch64/libkrun.so.2.0.0
context:lfs_pointer git-lfs object not fetched
lib/linux-aarch64/libkrunfw.so.5.5.0
context:lfs_pointer git-lfs object not fetched
lib/linux-x86_64/libkrun.so
context:lfs_pointer git-lfs object not fetched
lib/linux-x86_64/libkrun.so.2.0.0
context:lfs_pointer git-lfs object not fetched
lib/linux-x86_64/libkrunfw.so.5.5.0
context:lfs_pointer git-lfs object not fetched
lib/windows-x86_64/krun.dll
context:lfs_pointer git-lfs object not fetched
lib/windows-x86_64/libkrunfw.dll
context:unanalyzable_binary elf content is only byte-scanned
scripts/rosetta/rosetta-wrapper
no_dataflow_for_rust rust file: string-literal extraction only
crates/smolvm-agent/src/main.rs
no_dataflow_for_dynamic_code line 43: invoked code is not structurally extracted
docs/local-api/scripts/lifecycle-check.sh
no_dataflow_for_dynamic_code line 62: invoked code is not structurally extracted
docs/local-api/scripts/serve-start.sh
ledger_missing run 1: declared-vs-actual.0 SRC:8d0757918c1a77c9
ledger_missing run 1: declared-vs-actual.0 SRC:ae07981bf9d4f651
ledger_missing run 1: declared-vs-actual.0 SRC:b9d0b0e89cb475a0
ledger_missing run 1: declared-vs-actual.0 SRC:5812e0b097704867
ledger_missing run 1: declared-vs-actual.0 SRC:14f9d2c2166413e3
ledger_missing run 1: declared-vs-actual.0 SRC:cbc653024a933f97
ledger_missing run 1: declared-vs-actual.0 SRC:befa3e31381f8f4c
ledger_missing run 1: declared-vs-actual.0 SRC:74faffd2ef05e54e
ledger_missing run 1: declared-vs-actual.0 SRC:d9489b2cb841d850
ledger_missing run 1: declared-vs-actual.0 SRC:c433e2be622e6c8f
ledger_missing run 1: declared-vs-actual.0 SRC:5d719752a5c4536d
ledger_missing run 1: declared-vs-actual.0 SRC:f4140d1a6fc40efc
ledger_missing run 1: declared-vs-actual.0 SRC:23c03eaffb427def
ledger_missing run 1: declared-vs-actual.0 SRC:1f2c9ca7ba45b659
ledger_missing run 1: declared-vs-actual.0 SRC:50d1e0b0de437c74
ledger_missing run 1: declared-vs-actual.0 SRC:2aa76b2ea3104f7b
ledger_missing run 1: declared-vs-actual.0 SRC:352f5ebf682d751c
ledger_missing run 1: declared-vs-actual.0 SRC:39685ea9a7fb08a9
ledger_missing run 1: declared-vs-actual.0 SRC:41ddec502e1196cd
ledger_missing run 1: declared-vs-actual.0 SRC:435e57d743d9eb95
ledger_missing run 1: declared-vs-actual.0 SRC:4a842e3af8e40b07
ledger_missing run 1: declared-vs-actual.0 SRC:4d3af23c64b8a75d
ledger_missing run 1: declared-vs-actual.0 SRC:4f7626519803cf58
ledger_missing run 1: declared-vs-actual.0 SRC:5545923d49b6420e
ledger_missing run 1: declared-vs-actual.0 SRC:55ac3f9a9a2b7b6a
ledger_missing run 1: declared-vs-actual.0 SRC:6a77b6d249441750
ledger_missing run 1: declared-vs-actual.0 SRC:7217e7548ccceddf
ledger_finding_without_valid_finding run 1: behavior-chains.0 SRC:ae07981bf9d4f651
ledger_finding_without_valid_finding run 1: behavior-chains.0 SRC:5812e0b097704867
invalid_judge_pass run 1: ['declared-vs-actual.0']
invalid_judge_finding behavior-chains.0
request_unanswered request.4: behavior-chains.1/req-preflight-sh: resolved_clean: citation not shown or quote not in it
local-api (Skill) | Beacon Intel