Skip to content

fix: installer auto-stops rampart before upgrade#121

Merged
peg merged 83 commits intomainfrom
staging
Feb 26, 2026
Merged

fix: installer auto-stops rampart before upgrade#121
peg merged 83 commits intomainfrom
staging

Conversation

@peg
Copy link
Copy Markdown
Owner

@peg peg commented Feb 26, 2026

Prevents 'Access Denied' errors during upgrade when serve is running.

Changes

  • Try rampart serve stop gracefully before deletion
  • Kill any remaining rampart processes with Stop-Process
  • Add 500ms delay for Windows to release file handles

Now users can just re-run the installer without manually stopping serve first.

peg added 30 commits February 24, 2026 00:56
call_count was added to the engine in v0.4.8 but the linter's
validConditionFields map was never updated. Any policy using
call_count (including standard.yaml's rate-limit-fetch rule)
would get a lint error from rampart doctor and rampart policy lint.

Adds regression test.
rampart bench with no arguments looked for bench/corpus.yaml relative
to the current working directory. This always fails for installed users
(go install, binary download) who are not in the repo root.

Fix: embed corpus.yaml into the binary via go:embed. When --corpus is
not explicitly set, use the embedded bytes directly. --corpus still
accepts a custom file path as before.

Output shows 'Corpus: built-in' when using embedded corpus.
Adds regression test.
1. audit: expandHome missing from listAuditFiles and listAnchorFiles
   All rampart audit subcommands (tail/verify/stats/search/replay) crashed
   with the default --audit-dir because ~ was never expanded. Fixed in
   the shared helpers so all paths are covered.

2. bench: approval-gated coverage always 0%
   require_approval expected corpus entries were never included in
   DenyTotal/ApprovalGated accounting. Coverage math now includes both
   deny and require_approval expected entries.

3. status: require_approval and webhook decisions dropped from today stats
   Both action types were silently skipped in todayEvents() switch.
   Now counted alongside deny (they are blocking decisions).

4. generate: verbose null/empty fields in output YAML
   Added omitempty to Policy.Priority, Enabled, Match.Agent, Match.Session,
   and all Condition slice/bool fields. Marshaling-only change — parsing
   of existing policies is unaffected.
rampart upgrade returned early when current == target, skipping the
policy update step. Users who upgraded binaries manually or who were
already on the latest version never got policy improvements from newer
releases without knowing to run rampart init --force separately.

Now always runs upgradeStandardPolicies unless --no-policy-update is set.
Adds regression test.
rampart serve --background generated a fresh random token on every start,
ignoring ~/.rampart/token and never writing to it. Only the systemd/launchd
install path (serve install) called resolveServiceToken, which reads the
persisted file. The foreground serve path only checked RAMPART_TOKEN env.

Fix: before creating the proxy, read the persisted token (RAMPART_TOKEN >
~/.rampart/token > generate new). After the listener is bound, write the
token back to ~/.rampart/token so it survives restarts.

Adds regression test: TestServeReadsAndPersistsToken — starts serve with a
pre-written known token, verifies the same token is used and re-persisted.
- tests/e2e.yaml: 31-case test suite covering allow/deny/require_approval/watch
  across destructive cmds, FP regression, credentials, network exfil, env injection.
  Run: rampart test tests/e2e.yaml (uses installed policy)
  or:  rampart test --config policies/standard.yaml tests/e2e.yaml (repo policy)

- policies/standard.yaml: three policy gaps fixed:
  * block-credential-access: /etc/shadow, /etc/passwd, /etc/sudoers, /etc/gshadow
    now blocked for the read tool (were only blocked via exec patterns)
  * block-credential-commands: scp/rsync of private keys now blocked via
    combined command_contains('.ssh/id_') + command_matches('scp *'|'rsync *')
  * block-credential-commands: .pub exclusion added to SSH key rule to fix
    regression introduced by the scp rule split

- internal/engine/testrunner.go: expandHome for policy: path in test YAML
  (tilde was resolved relative to test file dir, not home directory)

- cmd/rampart/cli/test_cmd.go: --config flag now overrides policy: in test YAML,
  allowing 'rampart test --config dev-policy.yaml tests/e2e.yaml' workflows
- policies/standard.yaml: properly split block-credential-commands into 3 rules
  (ssh key cmds with .pub exclusion, scp/rsync transfer — dropped as FP-prone,
  aws/git/etc creds), add /etc/shadow|passwd|sudoers to read-tool block
- tests/e2e.yaml: 30 cases, all passing — removed scp test (gap noted)
- test_cmd.go: --config overrides policy: in test YAML
- testrunner.go: expandHome for policy path
- Narrow cat/head/tail patterns from /**/.ssh/** to /**/.ssh/id_* (too broad)
- Add matching exclusion patterns for .pub files (cat **/.ssh/*.pub etc)
- Add dedicated scp/rsync exfil rule with proper glob patterns
  - *scp*/.ssh/id_* matches exfil, excludes scp -i (auth)
  - *rsync*/.ssh/id_* matches exfil
- E2E tests: 36 cases including scp/rsync edge cases

All 36 e2e tests pass. All Go unit tests pass.
Fixes found by automated code review:

Security fixes:
- Remove -i flag exclusion from scp/rsync rule (prevents dual-key bypass:
  'scp -i auth_key exfil_key remote:' was incorrectly allowed)
- Add sftp to blocked transfer commands
- Add mv, xxd, hexdump, od, strings to blocked read commands
- Use *mv* pattern (leading wildcard) so mv with dest arg matches

Tradeoff:
- scp -i auth usage now blocked (security > convenience)
- Users needing -i can use ssh-agent or local policy override

Test updates:
- Updated scp -i test expectation (now intentionally blocked)
- All 36 e2e tests pass
- All Go unit tests pass
Add block-self-modification rule to standard.yaml that prevents AI agents
from running rampart allow/block/rules/policy commands. These commands
must be run by humans, not agents.

This prevents the attack where an agent sees 'run rampart allow' in a
denial message and tries to bypass its own restrictions.

Tests: 4 new e2e tests, 40/40 passing
…reload API

Merged features:
- rampart allow/block: Add custom allow/deny rules via CLI
- rampart rules: List, remove, reset custom rules
- POST /v1/policy/reload: Force immediate policy reload with rate limiting
- Denial suggestions: Show safe rampart allow commands in deny messages

Fixes applied from code review:
- Rate limiting on reload endpoint (1s cooldown)
- URL detection in IsPathPattern (curl https://... not a path)
- --tool flag override fixed in AddRule
- Atomic file writes (temp + rename)
- sudo/env wrapper detection in dangerous command check
- strconv.Atoi for strict integer parsing
- Mutual exclusion for --global/--project flags

TODO: Rewrite rules_test.go for new CustomPolicy structure
- CHANGELOG.md: add v0.5.0 release entry with Added/Changed/Fixed sections
- docs-site/getting-started/quickstart.md: add Customize Your Rules section
  with allow/block/rules quick start and self-modification protection note
- docs-site/features/policy-engine.md: add Custom Rules section documenting
  custom.yaml, allow/block, scopes, denial suggestions, self-mod protection,
  and the /v1/policy/reload API endpoint
- docs-site/guides/customizing-policy.md: new full guide covering patterns,
  scopes, flags reference, team sharing, denial workflow, and self-mod protection
- mkdocs.yml: add Customizing Policy to both Security Guides nav sections
- status: Boxed dashboard with progress bar, live server detection,
  and allow/deny/pending stats (lipgloss-powered, color + no-color safe)
- doctor: Structured 'Try this:' hints on failures/warnings, embedded
  in messages with hintSep so JSON output gets a 'hint' field too
- upgrade: Animated spinner during download and install phases;
  better permission error with sudo hint
- serve: Startup spinner with clean 'Rampart ready' message and
  emoji-labeled dashboard/token lines
- New spinner.go: lightweight braille-dot spinner (no bubbles dep),
  degrades to plain text on non-TTY outputs
- Tests updated to match new status box output format
- rampart policy generate preset: Interactive wizard with 4 presets
  (coding-agent, research-agent, ci-agent, devops-agent)
- Uses charmbracelet/huh for interactive forms
- 20 tests for policy generate, 26 tests for rules command
- Flags: --preset, --dest, --force, --print
…...'

- Add commonCommands map to detect known executables
- 'go build ./...' now correctly detected as exec, not path
- Handle './...' Go package patterns specially
- Add comprehensive tests for command/path detection
When using --global or --project filters with 'rampart rules',
the displayed indices now match the global index used by
'rampart rules remove'. This prevents confusion where index 1
in a filtered view would remove a different rule than expected.
- README: Add 'Customizing rules' section showing allow/block workflow
- CLI reference: Add full documentation for allow, block, rules, policy generate preset
- Homepage: Update FAQ to mention 'rampart allow' command
peg added 29 commits February 26, 2026 02:33
Normalize backslashes to forward slashes in MatchGlob so that policy
patterns like '**/.ssh/id_*' match Windows paths like
'C:\Users\Trevor\.ssh\id_rsa'.

This is critical for Windows users using Claude Code with Rampart —
without this fix, path-based policies would never match on Windows.

Added tests for Windows path patterns to verify cross-platform matching.
Security fixes:
- Move backslash normalization to cleanPaths() before filepath.Clean
  (fixes audit integrity issue on Unix with paths like /home/user\../etc)
- Add Windows token file security warning (os.Chmod is no-op on Windows)
- Add backslash injection test cases

Cursor/Windsurf fixes:
- Fix hardcoded Cursor docs URL shown to Windsurf users
- Add URL field to mcpServer for SSE-based servers
- Skip SSE servers with warning (cannot be wrapped)
- Only create backup if none exists (preserve true original)
- Remove dead init() function
- Update setup --help to list Cursor and Windsurf
Cursor and Windsurf have native built-in tools (file read/write, terminal)
that don't go through MCP. The setup commands only wrapped MCP servers,
giving users false confidence their agents were protected when 90%+ of
tool calls were unmonitored.

Removed:
- rampart setup cursor
- rampart setup windsurf
- cursor/windsurf auto-detection in quickstart
- All related tests and docs

Users wanting MCP-only protection can still use 'rampart mcp --' manually.
Focus remains on agents with real hook APIs (Claude Code, Cline) where we
can intercept all tool calls.

Also removed dead 'go generate ./...' from goreleaser (no go:generate
directives exist in the codebase).
- install.ps1: one-liner install for Windows users
- Downloads latest release from GitHub
- Extracts to ~/.rampart/bin, adds to PATH
- Auto-detects Claude Code and offers to set up hooks
- No admin rights required

Usage: irm https://rampart.sh/install.ps1 | iex
- docs-site/getting-started/installation.md: Add Windows tab with PowerShell
  installer, document Windows limitations table
- docs-site/integrations/cursor.md: Add warning that only MCP servers are
  protected, not Cursor's native built-in tools
- docs/guides/windows.md: Standalone Windows setup guide
- docs/install.ps1: Copy for GitHub Pages hosting at rampart.sh/install.ps1
- token_windows.go: Proper Windows ACL implementation using advapi32.dll
  Sets owner-only GENERIC_ALL access, removes all other permissions
- token_unix.go: Standard chmod 0600/0700 for Unix platforms
- Remove 'future version' comments - it's implemented now
- Update Windows service messages to be more helpful (suggest NSSM/Task Scheduler)
- Update docs to remove 'planned for future' language
The previous implementation used hand-rolled Windows API calls via
advapi32.dll with the unsafe package. This had risks:
- Struct alignment must exactly match Windows C structures
- Memory management with LocalFree
- No easy way to test correctness

New implementation uses icacls.exe (built into Windows):
- icacls path /inheritance:r /grant:r USERNAME:F
- Removes inherited permissions
- Grants full control only to current user
- Well-tested Windows utility
- No unsafe package needed
- Graceful fallback if icacls fails

-108 lines of complex syscall code, +23 lines of simple exec.
The hook evaluates policies locally — no server needed for allow/deny.
Serve is only required for:
- Live dashboard (rampart watch)
- Approval flow (require_approval policies)
- Centralized audit streaming

Updated:
- install.ps1: Simplified next steps, serve marked as optional
- docs/guides/windows.md: Reframed serve as optional
- docs-site/getting-started/installation.md: Changed note to success callout
rampart uninstall:
- Removes hooks from Claude Code, Cline
- Stops and removes systemd/launchd services
- Removes from PATH (Windows: programmatic, Unix: prints instructions)
- Removes shell shim if present
- Prints final instructions to delete ~/.rampart

Works on Windows, macOS, and Linux. Use --yes to skip prompts.
- Windows: Use PowerShell to find and stop rampart serve processes
- macOS/Linux: Use pkill -f 'rampart serve' before service removal

This ensures 'rampart uninstall' cleans up running processes on all platforms.
Fixes 'ExtractToFile: file already exists' error when reinstalling.
Some PowerShell versions/configurations use .NET extraction methods
that don't honor -Force overwrite flag.
- Detect and explain permission errors when removing existing install
- Clean up partial install on extraction failure
- Provide takeown/icacls recovery commands
- Fix icacls command quoting in error message ($env:USERNAME:F → $($env:USERNAME):F)
- Add cmd.exe rd fallback when PowerShell Remove-Item fails
- Better formatting for recovery instructions
- Remove 'restart terminal' instruction (PATH refreshes in-session)
- Always refresh session PATH (not just when adding)
- Update uninstall instruction to use 'rampart uninstall'
- Show try-it-now commands after install
- Add SmartScreen/Defender/antivirus bypass instructions
- Add permission error recovery steps
- Update uninstall to use 'rampart uninstall'
Previously we only hooked Bash, Read, Write|Edit. Now we use '.*' to
catch all tools including Fetch, Task, and any future tools Claude adds.

This ensures comprehensive coverage without needing to update Rampart
when Anthropic adds new tool types.

Users should run 'rampart setup claude-code --force' to upgrade their
hooks to the new wildcard matcher.
On reinstall, if Rampart hooks already exist in Claude Code settings,
prompts 'Update hooks to latest version?' and runs with --force.

This ensures users get new hook features (like wildcard matcher) when
upgrading Rampart.
High: Add SHA256 checksum verification to Windows installer
- Downloads checksums.txt from release and verifies downloaded zip
- Fails with clear error on mismatch, warns if checksums unavailable

Medium: Fix PowerShell single-quote injection in uninstall.go
- Escape single quotes in PATH value before passing to PowerShell
- Prevents injection via crafted PATH entries like C:\Users\O'Brien\

Low: Use os.Executable() instead of PATH lookup in uninstall
- Prevents malicious 'rampart' binary earlier in PATH from executing

Low: Improve upgrade detection regex in install.ps1
- Match specific 'rampart hook' command pattern, not just 'rampart' substring
- install.ps1: Add (?:\.exe)? to match both 'rampart hook' and 'rampart.exe hook'
- setup.go hasRampartInMatcher: Handle Windows backslash paths and .exe extension

Fixes regression where Windows installs weren't detected as upgrades.
SSE connections were blocking server shutdown, causing 'context deadline exceeded'
errors on Ctrl+C. Now we close all SSE clients before shutting down the HTTP server.

Also fixed potential double-close panic in unsubscribe by checking if channel still exists.
- Add 'closed bool' field to prevent post-Close subscriptions
- subscribe() returns immediately-closed channel if hub is closed
- broadcast() skips if hub is closed
- Add 5 tests covering shutdown scenarios with -race flag

Closes the race window between sse.Close() and srv.Shutdown().
- Try 'rampart serve stop' gracefully before deletion
- Kill any remaining rampart processes
- Add 500ms delay for Windows to release file handles

This prevents 'Access Denied' errors during upgrade when serve is running.
Windows Defender or the file indexer can briefly lock files after a process
exits. Adding a small delay before serve returns gives the OS time to release
handles, preventing 'Access Denied' errors on subsequent operations.
@peg peg merged commit 3eb5f54 into main Feb 26, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant