Zwischen

CI Gem Version npm PyPI License: MIT

Zwischen blocking a push that contains an AWS secret

Zwischen blocks git push the moment you're about to leak a secret — the last point where a leaked credential is still a local problem instead of an incident. It orchestrates Gitleaks and Semgrep, normalizes their findings, and (optionally) asks an AI provider — including fully local models via Ollama — to prioritize the results, flag false positives, and suggest fixes.

One command sets everything up:

zwischen init   # installs gitleaks if missing, creates config, installs the pre-push hook

Try it on a deliberately vulnerable app

The zwischen-demo repository is a small Express app seeded with fake secrets and real vulnerability patterns. Clone it and watch a push get blocked in under a minute.

Installation

gem install zwischen        # canonical implementation (Ruby)
npm install -g zwischen     # Node wrapper
pip install zwischen-cli    # Python wrapper (command is still `zwischen`)

For local development from this repository:

bundle install
bundle exec ruby -Ilib bin/zwischen --help

Quick start

Run these from the project you want to protect:

zwischen init     # config + pre-push hook + gitleaks auto-install
zwischen scan     # manual scan with full report
zwischen doctor   # check tool status

AI triage

Raw scanner output tells you what matched. The AI pass tells you what to fix first and how — see the real before/after in docs/triage-example.md, generated with a local model via Ollama.

Provider Flag Setup
Claude --ai claude Set ANTHROPIC_API_KEY or pass --api-key.
Ollama --ai ollama Install Ollama and pull a model. Local-first: nothing leaves your machine.
OpenAI --ai openai Set OPENAI_API_KEY or pass --api-key.

Manual scans use AI when --ai is passed or config enables it. Pre-push scans stay scanner-only unless ai.pre_push_enabled: true — blocking decisions should be fast and deterministic. The design rationale is in docs/design.md.

Commands

Command What it does
zwischen init Installs/checks tools, creates config, installs pre-push hook (backs up an existing non-Zwischen hook).
zwischen scan Runs enabled scanners, prints a terminal report.
zwischen scan --changed Scans only files changed since the default branch.
zwischen scan --only secrets,sast Limits to Gitleaks (secrets) and/or Semgrep (sast).
zwischen scan --ai <provider> Adds AI prioritization, fix suggestions, false-positive detection.
zwischen scan --format json Machine-readable summary + findings.
zwischen scan --format sarif SARIF 2.1.0 for GitHub code scanning.
zwischen scan --pre-push Quiet hook mode: changed files only, compact output only when blocking.
zwischen doctor Shows Gitleaks and Semgrep status.
zwischen uninstall Removes the hook, optionally config/credentials.

Escape hatches: git push --no-verify or ZWISCHEN_SKIP=1 git push.

GitHub Action

Run the same scan in CI and feed the GitHub Security tab:

- uses: cjordan223/zwischen@main
  with:
    sarif-file: zwischen.sarif
- uses: github/codeql-action/upload-sarif@v3
  if: always()
  with:
    sarif_file: zwischen.sarif

Configuration

Create or edit .zwischen.yml in the scanned project:

ai:
  enabled: true
  pre_push_enabled: false
  provider: claude          # claude, ollama, or openai
  ollama:
    model: llama3
    url: http://localhost:11434
    timeout: 180            # seconds; local models can be slow to load

blocking:
  severity: high            # high, critical, or none

scanners:
  gitleaks:
    enabled: true
  semgrep:
    enabled: true
    config: p/security-audit,p/expressjs   # comma-separated rulesets

ignore:                     # findings under these globs are dropped
  - "**/node_modules/**"
  - "**/test/fixtures/**"

Boolean scanner entries (gitleaks: true) work too. Credentials are read from environment variables first, then ~/.zwischen/credentials (written by zwischen init when ANTHROPIC_API_KEY is set).

Architecture

flowchart LR
    push[git push] --> hook[pre-push hook]
    hook --> diff[GitDiff<br/>changed files]
    diff --> orch[Orchestrator]
    cli[zwischen scan] --> orch
    orch --> gl[Gitleaks<br/>secrets]
    orch --> sg[Semgrep<br/>SAST]
    gl --> agg[Aggregator<br/>normalize + dedupe<br/>+ ignore globs]
    sg --> agg
    agg --> ai{AI enabled?}
    ai -- yes --> llm[Claude / OpenAI / Ollama<br/>priority · fixes · false positives]
    ai -- no --> rep
    llm --> rep[Reporter]
    rep --> term[terminal / compact]
    rep --> mach[json / sarif]
    term --> block{blocking<br/>finding?}
    block -- yes --> deny[⛔ push blocked]
    block -- no --> allow[✓ push proceeds]

The Ruby gem is the canonical implementation; the npm and pip packages are convenience wrappers with a smaller command surface.

Wrapper parity

Capability Ruby gem npm / pip wrappers
init / scan / doctor
uninstall
--only scanner selection
--changed / changed-file pre-push filtering — (hook scans the project)
--format json
--format sarif
AI providers claude, ollama, openai ollama, openai, anthropic

The wrapper gaps are intentional scope decisions, not bugs: the wrappers exist so npm install -g zwischen / pip install zwischen-cli work in ecosystems where a Ruby gem is friction, and they track the core workflow (init → hook → scan) rather than every flag.

Repository layout

bin/zwischen                  Ruby executable
lib/zwischen/                 Ruby gem implementation
lib/zwischen/scanner/         Gitleaks and Semgrep adapters
lib/zwischen/ai/              Claude, Ollama, and OpenAI clients
lib/zwischen/reporter/        Terminal and SARIF reporters
packages/npm/                 Node wrapper package
packages/pip/                 Python wrapper package
spec/                         RSpec suite
action.yml                    Composite GitHub Action
docs/                         Design write-up, triage example, demo GIF

Development

bundle exec rspec             # 196+ examples
./scripts/test_as_gem.sh      # install and exercise as a real gem

See DEVELOPMENT.md for architecture notes and the release process, and TESTING.md for the end-to-end test plan.

License

MIT