Repository navigation
Releases
pwner 0.3.3
The loop now has a reason to keep digging
Every guard in the agent loop pushed it to stop: the identical-batch counter, the identical-result counter, the call-site window. Nothing balanced them, so the cheapest way for a model to end a run was to stop calling tools, and the loop agreed however much it had left open. A scan that gave up early and a scan that finished looked the same from outside.
The termination boundary asks now. When the model emits no tool calls, what your ledger still has outstanding comes back to it as one more turn: unresolved hypotheses, and services recorded as surface that nothing you tried mentions. Three properties keep it from becoming a leash. It reports state and never an instruction — it names no tool and prescribes no action. It is capped at two. And deciding a hypothesis is not worth chasing is a complete answer. A shallow stop becomes visible without becoming impossible.
Hypotheses are records. ledger() holds them with an id, the evidence behind them, how the agent said it would test them, and a status it can resolve. A bare sentence is still accepted and promoted to a record, because refusing them would only stop the agent writing down the half-formed suspicions worth the most. They outlive context compaction; the transcript they used to live in does not. show takes a filter too, by status or free text, so joining two observations thirty steps apart no longer depends on both still being in the window.
Reasoning budget follows the evidence. /effort low|medium|high|off sets it for the session instead of the launch-time environment variable. A tool that found something says so in its result: param_probe when a parameter moved the response in a way the baseline does not explain, segmentation_test when the boundary did not hold or could not be read. The loop raises the budget one notch for the next few steps and knows nothing about which tool means what. Still unset by default, because a model that does not implement reasoning_effort answers 400 to it rather than ignoring it.
None of this is a rule in the system prompt. One line there changed, and only to stop pointing hypotheses at note() now that there is a better place for them.
Tool calls in one batch run at once
A model that asks for four port scans in one message meant them to happen together; they used to run end to end, so the batch cost the sum of its parts. The ordering providers enforce is of the tool responses, which is preserved, and a cancelled batch still answers every tool_call_id.
Fixes
- Two tools asking for consent at the same moment would have raced for one line of stdin. The operator would have seen two questions, answered one, and which one got the answer would have been luck. Prompts now serialize on a lock and re-check for a remembered approval after taking it. Latent until something ran concurrently, which is now.
/effortdeliberately is not saved to your config: a stored "high" would quietly bill every later session for deliberation it did not need.
52 tools — this release adds none, it changes what the loop does with the ones there are. 785 tests, 60 of them new, across Python 3.10 to 3.13.
Authorized use only. pwner sends real traffic to the hosts you name and, with your approval, actively breaks into them.
pwner 0.3.2
pip install pwner
pwner is on PyPI from this release, so the three install routes are now:
pip install pwner # PyPI
brew install 0x45dd/tap/pwner # Homebrew
curl -fsSL https://pwner.si/install.sh | bashnmap is still the only required external binary; pwner doctor reports what else is missing.
Packaging
PyPI renders README.md but does not rewrite relative paths, so the logo, the session recording, the language bar and every link into the source tree would have been broken on the project page. All five READMEs now use absolute URLs, which behave identically on GitHub.
Metadata gained keywords, the per-version Python classifiers CI actually tests (3.10 through 3.13), and OS classifiers.
52 tools. 725 tests, of which 77 cover the scope boundary, 32 the scope-change gate and 13 the consent gate.
Authorized use only. pwner sends real traffic to the hosts you name and, with your approval, actively breaks into them.
pwner 0.3.1
The agent could widen its own scope. It cannot now.
Every tool call is checked against the target you named, by a dispatch wrapper that runs before the tool body. set_target has to be exempt from that check, because its argument is the new scope, so there is nothing to check it against. That left a path the guard could not see, and the refusal message pointed straight at it: "Only the operator can widen scope, ask them, then set_target". A model read that as an instruction and was observed doing exactly it, refusing on a host outside scope and then calling set_target on that host and scanning it.
set_target now carries its own gate, and it is specifically about scope rather than about risk:
- narrowing, or returning to something you already authorized, is free
- going anywhere else needs your answer, every time
- a remembered approval for some other action does not buy it
--auto-consentdoes not buy it either, because that approves high-impact actions inside the scope, not a new scope- a non-interactive run has nobody to ask, so the answer there is no
The refusal message no longer names the bypass, and a denial tells the agent to say what it needs in its reply rather than to try again. 32 tests in tests/test_scope_change.py, including the original attack reproduced as a regression test.
If you are on 0.2.0, this is the reason to upgrade. The 0.2.0 README claims the agent cannot widen its own authorization, and in that tree it could.
A real web surface
Three new tools, so a web target is no longer one request at a time:
web_crawl walks the application from a starting URL and returns the endpoints it found, grouped by surface rather than by URL, so /item?id=1 and /item?id=2 collapse into one endpoint with one parameter. It stays on the host and port you pointed it at, which is the crawler's half of the scope boundary.
param_probe sends one call per parameter and reads the result against a clean baseline for that same parameter. Everything is differential: a reflection only counts when the tag characters survived, and a length change that the echoed payload already explains is not a signal. Against a deliberately broken local app this cut 20 noisy rows down to the 3 that were real.
dirsearch_scan drives dirsearch for content discovery when it is installed, reported by pwner doctor alongside the rest.
The agent's web methodology now points at these instead of at bare HTTP requests. No new rules in the prompt: the capability is in the tools.
Fixes
--jsonprinted the ASCII banner, sopwner pci --json > evidence.jsonproduced a file that was not JSON. The banner is suppressed for every--jsonsubcommand now.max_depth=0on a crawl meant "just this page" and was being read as "use the default of 3", because the default was applied withor.- The consent gate's own documentation was wrong in all five READMEs: it said a non-interactive run always refuses, when
--auto-consentapproves there. The scope gate does refuse unconditionally; the consent gate does not.
Also
--auto-consent's help text no longer implies it can authorize a scope change.- Scope decisions are recorded in their own list rather than in the consent record, so approving a new target never reads as blanket consent for brute force.
52 tools. 725 tests, of which 77 cover the scope boundary, 32 the scope-change gate and 13 the consent gate. Python 3.10 to 3.13.
Authorized use only. pwner sends real traffic to the hosts you name and, with your approval, actively breaks into them.
(0.3.0 exists as a tag that was never released: it was cut one commit early, before the README correction above. v0.3.1 is that release.)
pwner 0.2.0
PCI DSS segmentation testing
pwner pci <cde> runs a PCI DSS 11.4.5 segmentation test, or 11.4.6 on the six-month service-provider cycle, with no model and no agent. Run it from a segment that is out of scope for the CDE and it reports whether the boundary held, exiting 0 isolated, 2 not isolated, 3 inconclusive, so it works as a gate.
It is not the question a port scan asks. A port scan wants to know what is listening; this wants to know whether a packet can arrive at all, so a closed port that answers with a RST is a failure of isolation rather than a pass. A TCP connect cannot tell a host RST from a firewall reject, so that case is reported as inconclusive instead of being guessed in either direction, and an inconclusive probe is never filed as a finding.
pci_retest covers the repeat half of 11.4.4 by re-probing findings already on the record. Neither tool makes anyone compliant: 11.4.2 and 11.4.3 require a qualified tester with organizational independence, 11.4.1 a documented methodology, and the quarterly external scan in 11.3.2 an Approved Scanning Vendor.
Choosing a model without restarting
/model and /provider change the model inside a running session, keeping the target, the findings and the conversation. pwner --pick offers the menu at launch, and the plain pwner launch stays instant.
Local models
Lemonade joins Ollama as a preset, and any other OpenAI-compatible server (llama.cpp, LM Studio, vLLM) works through --base-url. A local endpoint needs no API key.
One-command install
curl -fsSL https://pwner.si/install.sh | bashBuilds its own virtualenv under ~/.local/share/pwner, leaves the system Python alone, and puts pwner on your PATH. --with-tools installs the scanners too, --uninstall takes it all back out.
Fixes
- Ollama and Lemonade users were sent through the setup wizard on every launch, because "already configured" was decided by the presence of an API key and a server on your own hardware has none.
- A socket timeout was classified as a generic error rather than a dropped packet on Python 3.10, where
asyncio.TimeoutErroris not the builtinTimeoutError. On a working segmentation boundary that turned the most common outcome into "inconclusive". nuclei -update-templates -ducfetched zero templates, so the Docker image and the dependency installer were both shipping none. Measured: 0 with the flag, 14,104 without.- A web server that answers every path with the same page no longer exhausts the context window. Identical responses are grouped and the catch-all is named.
- The agent no longer spends dozens of steps hitting one endpoint with a new payload each time. A call-site window catches it in 12 calls instead of 60.
- A context window that overflows now compacts the transcript and retries, instead of ending the run.
Also
- Kali is detected as its own package ecosystem, so
pwner doctorhands you apt commands rather than the source builds a plain Debian box needs. The Docker image is based on Kali. - README in Spanish, German, Japanese and Turkish.
49 tools. 626 tests, of which 77 cover the scope boundary and 13 the consent gate. Python 3.10 to 3.13.
Authorized use only. pwner sends real traffic to the hosts you name and, with your approval, actively breaks into them.