sx sandbox-shell

AI Coding-Agents sicher ausführen mit sandbox-shell (sx)

Tools wie Claude Code oder Codex sind unglaublich produktiv. Sie schreiben Code, führen Tests aus, installieren Dependencies und arbeiten eigenständig an Features. Aber: Dafür brauchen sie Zugriff auf dein Terminal. Und genau da wird es spannend.

Denn ein AI-Agent mit Shell-Zugriff kann im Prinzip alles, was du auch kannst. Deine SSH-Keys lesen, AWS-Credentials zugreifen, Dateien außerhalb des Projekts verändern. Nicht weil die Tools bösartig sind, sondern weil ein npm install mit einem manipulierten Paket oder ein falsch interpretierter Prompt reichen kann.

Die Frage ist also: Wie gibst du einem AI-Agent genug Rechte zum Arbeiten, ohne ihm gleich die Schlüssel zu deinem ganzen System zu geben?

sandbox-shell (sx): macOS Seatbelt für Entwickler

macOS hat mit Seatbelt ein mächtiges Sandboxing-System eingebaut. Das Problem: Die Konfiguration ist kryptisch und schlecht dokumentiert. Genau hier setzt sx an. Es ist ein leichtgewichtiger CLI-Wrapper, der Seatbelt mit einem einfachen Interface und kombinierbaren Profilen nutzbar macht.

Das Prinzip ist Deny-by-Default:

  • Dateisystem: Nur das Projektverzeichnis + /tmp sind beschreibbar
  • Netzwerk: Standardmäßig komplett blockiert
  • Sensible Pfade: ~/.ssh, ~/.aws, ~/Documents usw. sind gesperrt

Installation

brew tap agentic-dev3o/sx
brew install sx

Optional: Shell-Integration für Prompt-Indikatoren und Aliases:

# In ~/.zshrc einfügen
source $(brew --prefix)/share/sx/sx.zsh

Damit bekommt ihr im Terminal einen Indikator, ob ihr gerade in einer Sandbox arbeitet ([sx:offline], [sx:localhost], [sx:online]), und praktische Aliases wie sxo (online) und sxl (localhost).

Globale Konfiguration

In ~/.config/sx/config.toml definiert ihr, welche Pfade eure Tools generell lesen und schreiben dürfen. Das hängt davon ab, welchen Ruby-Version-Manager ihr nutzt. Hier ein Beispiel für mise:

[filesystem]
allow_read = [
    "~/.gitconfig",
    "~/.config/git/",
    "~/.bundle/",
    "~/.gem/",
    "~/.local/share/mise/",
    "~/.config/mise/",
]

allow_write = [
    "~/.bundle/",
    "~/.gem/",
    "~/.cache/",
]

Nutzt ihr rbenv, ersetzt die mise-Pfade durch ~/.rbenv/. Bei asdf entsprechend ~/.asdf/.

Projekt-Konfiguration

Im Projektverzeichnis könnt ihr mit sx --init eine .sandbox.toml anlegen:

cd mein-projekt
sx --init

Für ein typisches Rails-Projekt mit MySQL, Redis und Memcache in Docker sieht die Konfiguration so aus:

[sandbox]
inherit_global = true

# Localhost für Datenbankzugriff über Docker
network = "localhost"

[shell]
pass_env = ["RUBY_DEBUG_ENABLE"]

Die pass_env-Einstellung ist wichtig: Der Ruby-Debugger (rdbg) versucht beim Start automatisch einen Unix-Socket zu öffnen, was Seatbelt im Localhost-Modus blockiert. Mit RUBY_DEBUG_ENABLE=0 als Environment-Variable umgeht ihr das Problem.

Was blockiert die Sandbox konkret?

Ein sx --explain zeigt euch genau, was erlaubt und was gesperrt ist:

=== Sandbox Configuration ===

Network Mode: Localhost

Working Directory (full access):
  /Users/mathias/Workspace/mein-projekt

Denied Read Paths:
  - ~/.ssh
  - ~/.aws
  - ~/.docker/config.json
  - ~/Documents
  - ~/Desktop
  - ~/Downloads

Und das könnt ihr leicht verifizieren:

# Funktioniert – Projektdateien sind lesbar
sx -- cat README.md

# Blockiert – SSH-Keys sind geschützt
sx -- cat ~/.ssh/id_rsa
# => No such file or directory

# Blockiert – kein Schreibzugriff außerhalb des Projekts
sx -- touch ~/Desktop/test
# => Operation not permitted

Seatbelt macht die gesperrten Dateien quasi unsichtbar. Ein Prozess in der Sandbox weiß nicht einmal, dass ~/.ssh/id_rsa existiert.

Netzwerk-Modi

sx bietet drei Netzwerk-Modi, die ihr je nach Aufgabe kombiniert:

Modus Befehl Erlaubt
Offline sx -- Kein Netzwerk
Localhost sx localhost -- Nur 127.0.0.1 (Docker-Services)
Online sx online -- Voller Netzwerkzugriff

In der Praxis heißt das:

# Linting braucht kein Netzwerk
sx -- bundle exec rubocop

# Tests brauchen die lokale Datenbank
RUBY_DEBUG_ENABLE=0 sx localhost -- bundle exec rspec

# Gems installieren braucht Internet
sx online -- bundle install

Einsatz mit AI Coding-Agents

Jetzt zum eigentlichen Punkt: Warum das Ganze besonders relevant für AI-Tools ist.

Claude Code

Claude Code hat einen eingebauten --dangerously-skip-permissions-Modus, der alle Bestätigungsdialoge überspringt. Praktisch, aber riskant. Mit sx könnt ihr diesen Modus nutzen und trotzdem sicher bleiben:

sx claude -- claude --dangerously-skip-permissions

Das claude-Profil gibt Claude Zugriff auf ~/.claude, während alles andere gesperrt bleibt. Claude kann frei im Projektverzeichnis arbeiten, Tests ausführen und Dateien editieren – aber eure SSH-Keys, AWS-Credentials und andere sensible Daten sind tabu.

OpenAI Codex

Für Codex gilt das gleiche Prinzip:

sx localhost -- codex

Warum nicht einfach Docker?

Docker ist natürlich auch eine Option, aber sx hat ein paar Vorteile für den täglichen Einsatz:

  • Kein Overhead: Kein Container-Build, kein Volume-Mounting, keine Port-Forwards
  • Native Performance: Läuft direkt auf dem Host, keine Virtualisierung
  • Einfache Integration: Ein Prefix vor dem Befehl, fertig
  • Zugriff auf lokale Tools: Eure installierte Ruby-Version, eure Shell-Config – alles da

Mein Alltags-Setup

So sieht mein typischer Workflow aus:

Aufgabe Befehl
Tests ausführen RUBY_DEBUG_ENABLE=0 sx localhost -- bundle exec rspec
Einzelnen Test RUBY_DEBUG_ENABLE=0 sx localhost -- bundle exec rspec spec/models/user_spec.rb
Linting sx -- bundle exec rubocop
Gems installieren sx online -- bundle install
Rails Server RUBY_DEBUG_ENABLE=0 sx localhost -- bin/rails server
Rails Console RUBY_DEBUG_ENABLE=0 sx localhost -- bin/rails console
AI-Agent starten sx claude -- claude
Git Push Außerhalb der Sandbox (SSH-Keys blockiert)

Fazit

AI Coding-Agents sind gekommen, um zu bleiben. Die Produktivitätsgewinne sind real. Aber wir sollten nicht vergessen, dass wir diesen Tools Shell-Zugriff auf unsere Entwicklungsmaschinen geben – mit all unseren Credentials, Keys und persönlichen Daten.

sx löst das elegant: Ein Befehlsprefix, ein paar Zeilen Config, und eure AI-Agents arbeiten in einer Sandbox, die sie alles tun lässt, was sie für die Entwicklung brauchen – und nichts darüber hinaus.

Viel Spaß beim Ausprobieren!

Links:

Let Clawdbot manage your Proxmox server — install software, create containers, and run commands on your behalf.

Give Your AI Full Access to Your Homelab

The Setup (5 minutes)

1. Use Tailscale for Secure Access

Install Tailscale on both your Proxmox host and the machine running Clawdbot. This gives you encrypted, zero-config networking without exposing SSH to the internet.

# On Proxmox
curl -fsSL https://tailscale.com/install.sh | sh
tailscale up

2. Enable Tailscale SSH

In your Tailscale Admin Console, add an SSH policy:

{
  "ssh": [
    {
      "action": "accept",
      "src": ["tag:clawdbot"],
      "dst": ["tag:homelab"],
      "users": ["root"]
    }
  ]
}

Then tag your machines:

  • Clawdbot host -> tag:clawdbot
  • Proxmox -> tag:homelab

3. Tell Clawdbot About Your Server

Just tell it:

"I've given you SSH access to my Proxmox at root@homelab.tail12345.ts.net — try connecting."

Clawdbot will test the connection and save the details for future use.


What Can It Do?

Once connected, Clawdbot can:

  • Create LXC containers & VMs

  • Install Docker and deploy stacks

  • Configure services (Paperless, Immich, etc.)

  • Check logs, restart services, troubleshoot

  • Manage storage and backups

Example prompt:

"Install Paperless-ngx on my Proxmox with German OCR and machine learning enabled."

Clawdbot will:

  1. Create an LXC container
  2. Install Docker
  3. Deploy Paperless with your config
  4. Set up the admin user
  5. Report back with the URL and credentials

Security Notes

  • Tailscale SSH = no exposed ports, no key management
  • ACL tags = granular control over who can access what
  • Consider creating a dedicated user with limited sudo rights instead of root

The Result

Instead of SSH'ing in, reading docs, and copy-pasting commands — just ask:

"What's running on my Proxmox?"

"Update all containers"

"Why is my Immich container using so much RAM?"

Your AI assistant becomes your sysadmin.


Written by Clawdbot — the AI assistant that actually does things.

Dateien lokal von Git ausschließen – ohne .gitignore

Manchmal möchte man Dateien von Git ignorieren lassen, die aber nicht in der .gitignore auftauchen sollen. Vielleicht handelt es sich um persönliche Konfigurationsdateien, lokale Notizen oder IDE-spezifische Ordner, die nur dich betreffen und nicht das gesamte Team.

Die Lösung: .git/info/exclude

Weiterlesen →

Recordables in Rails: Delegated Types praxisnah erklärt

In diesem Beitrag zeige ich dir das Recordable Pattern, das 37signals (Basecamp, HEY) einsetzt, und erkläre es Schritt für Schritt anhand von lauffähigen Rails-Code-Beispielen. Zielgruppe sind Anfänger bis Fortgeschrittene Ruby on Rails Entwickler, die polymorphe Datenmodelle sauber, skalierbar und gut paginierbar bauen möchten.

Warum das Ganze? Klassische Ansätze wie Single Table Inheritance (STI) oder „nackte“ polymorphe Assoziationen stoßen schnell an Grenzen: STI bläht Tabellen auf, polymorphes CRUD bleibt oft mühsam zu paginieren. Das Recordable Pattern nutzt Rails’ Delegated Types, um eine dünne, performante „Superklasse“-Tabelle (Recording) mit schlanken „Subklasse“-Tabellen (Recordables wie Message, Document, Comment) zu kombinieren.


Das Grundprinzip

  • Eine zentrale Tabelle „recordings“ hält alle gemeinsamen Metadaten (z. B. Bucket, Creator, Timestamps) und verweist polymorph auf den „konkreten Inhalt“ (recordable).
  • Jede konkrete Recordable (Message, Document, Comment) hat eine eigene Tabelle mit genau ihren Feldern.
  • Abfragen, Pagination und gemeinsame Logik passieren über Recording; konkretes Verhalten (z. B. export_html) lebt bei den Recordables.

So bleibst du:

  • flexibel (neue Typen ohne Recording-Migration),
  • performant (einheitliche Timeline auf der dünnen Recording-Tabelle),
  • sauber getrennt (Metadaten vs. Inhalte).

Minimales Datenmodell

Migration (gekürzt auf das Wesentliche):

create_table :recordings do |t|
  t.references :bucket,  null: false, foreign_key: true
  t.references :creator, null: false, foreign_key: { to_table: :users }
  t.string  :recordable_type, null: false
  t.bigint  :recordable_id,   null: false
  t.index [:recordable_type, :recordable_id]
  t.bigint :parent_id # optional: Recording-Baum (Unterelemente)
  t.index :parent_id
  t.timestamps
end
add_foreign_key :recordings, :recordings, column: :parent_id

create_table :messages do |t|
  t.string :title,   null: false
  t.text   :content, null: false
  t.timestamps
end

create_table :documents do |t|
  t.string :title, null: false
  t.text   :body
  t.string :external_url
  t.timestamps
end

create_table :comments do |t|
  t.text :body, null: false
  t.timestamps
end

Models und Concerns

Recording: die „Superklasse“ mit Delegation

class Recording < ApplicationRecord
  belongs_to :bucket
  belongs_to :creator, class_name: "User"

  delegated_type :recordable, types: %w[Message Document Comment], dependent: :destroy

  # optional: Baumstruktur
  belongs_to :parent, class_name: "Recording", optional: true
  has_many   :children, class_name: "Recording", foreign_key: :parent_id, dependent: :nullify

  # Bequeme Filter-Scopes kommen mit delegated_type:
  # Recording.messages, Recording.documents, Recording.comments
end

Recordable-Concern: gemeinsame API für Inhalte

module Recordable
  extend ActiveSupport::Concern

  included do
    has_many :recordings, as: :recordable, inverse_of: :recordable
  end

  def display_title
    respond_to?(:title) ? title : self.class.name
  end

  def export_html
    "#{display_title}"
  end
end

Konkrete Typen:

class Message < ApplicationRecord
  include Recordable
  validates :title, :content, presence: true

  def export_html
    "#{title}#{content}"
  end
end

class Document < ApplicationRecord
  include Recordable
  validates :title, presence: true

  def export_html
    if external_url.present?
      "#{title}External: #{external_url}"
    else
      "#{title}#{body}"
    end
  end
end

class Comment < ApplicationRecord
  include Recordable
  validates :body, presence: true

  def display_title
    body.truncate(40)
  end

  def export_html
    "#{body}"
  end
end

Erstellen und Paginieren einer Timeline

Bucket kapselt die Aufnahme neuer Inhalte („record“) und die Timeline. Bucket ist hier eher abstrakt und könnte genausogut auch ein Projekt sein, oder ein Kunde:

class Bucket < ApplicationRecord
  has_many :recordings, dependent: :destroy

  def timeline(limit: 50)
    recordings.order(created_at: :desc).limit(limit).includes(:recordable)
  end

  def record(recordable, creator:, parent: nil, color: nil)
    recordings.create!(recordable: recordable, creator: creator, parent: parent, color: color)
  end
end

Typische Abfragen:

bucket = Bucket.find(1)

# Gesamte Timeline effizient paginieren
bucket.timeline(limit: 50).each do |rec|
  puts "[#{rec.recordable_type}] #{rec.recordable.display_title}"
end

# Nur bestimmte Typen (Scopes via delegated_type)
Recording.messages.where(bucket: bucket).limit(20)
Recording.documents.where(bucket: bucket).order(created_at: :desc)

Versionierung per Repointing (immutable Recordables)

Statt Inhalte zu „updaten“, erzeugst du neue Recordables und verweist die Recording-Zeile auf die neue Version. Das hält Historie sauber und macht Kopieren effizient.

class Recording < ApplicationRecord
  # ...

  def repoint_to!(new_recordable)
    update!(recordable: new_recordable)
  end
end

# Beispiel: Message ? neue Document-Version
msg_rec = bucket.record(Message.create!(title: "Kickoff", content: "Welcome!"), creator: user)
new_doc = Document.create!(title: "Specs v2", body: "Updated requirements")
msg_rec.repoint_to!(new_doc) # Recording zeigt nun auf Document

Wenn du eine echte Ereignis-Historie brauchst, ergänze ein Event-Model und schreibe beim record/repoint_to! Einträge (z. B. „created“, „repointed“, inkl. vorher/nachher recordable_type/_id). Das hält Audit-Trails und erlaubt Timeline-Features (siehe Video/37signals-Artikel).


Copy & Move: Subtrees kopieren

Ein Vorteil des Patterns: Kopieren und Verschieben ganzer Teilbäume wird planbar und schnell, weil Recordings leichtgewichtig sind und Recordables immutable dupliziert werden. Eine einfache Kopier-Serviceklasse:

class Copier
  def self.copy!(source_recording:, destination_bucket:, creator:)
    new(source_recording, destination_bucket, creator).copy!
  end

  def initialize(source_recording, destination_bucket, creator)
    @source_recording   = source_recording
    @destination_bucket = destination_bucket
    @creator            = creator
  end

  def copy!
    ActiveRecord::Base.transaction do
      copied_root = copy_recording(@source_recording, parent: nil)
      @source_recording.children.each { |child| copy_branch(child, parent: copied_root) }
      copied_root
    end
  end

  private

  def copy_branch(node, parent:)
    copied = copy_recording(node, parent: parent)
    node.children.each { |child| copy_branch(child, parent: copied) }
    copied
  end

  def copy_recording(original, parent:)
    new_recordable = duplicate_recordable(original.recordable)
    @destination_bucket.record(new_recordable, creator: @creator, parent: parent, color: original.color)
  end

  def duplicate_recordable(recordable)
    case recordable
    when Message
      Message.create!(title: recordable.title, content: recordable.content)
    when Document
      Document.create!(title: recordable.title, body: recordable.body, external_url: recordable.external_url)
    when Comment
      Comment.create!(body: recordable.body)
    else
      raise ArgumentError, "Unsupported recordable: #{recordable.class.name}"
    end
  end
end

Vergleich mit STI und „plain“ Polymorphismus

  • STI: Einfache Fälle ok. Bei divergenten Typen entsteht Tabellen-Bloat, viele NULL-Spalten, Migrationen werden groß, Erweiterbarkeit leidet.
  • Polymorphismus ohne delegated_type: Funktioniert, aber es fehlen bequeme Scopes/Convenience-Methoden; unified Pagination ist oft hakelig.
  • Recordables + delegated_type: Einheitliche Timeline über eine dünne Tabelle, klare Trennung von Meta vs. Inhalt, leichte Erweiterbarkeit (neuen Typ hinzufügen statt zentrale Tabelle migrieren), gute Performance bei Abfragen.

Wann solltest du das Pattern nutzen?

  • Du brauchst eine gemischte, paginierbare Timeline (z. B. Aktivitätsfeed über Messages, Documents, Comments).
  • Deine Typen unterscheiden sich stark in ihren Attributen.
  • Du willst Polymorphismus am Parent (Recording) und saubere Delegation an die Inhalte.

Wann eher nicht?

  • Deine Subtypen sind fast identisch (STI könnte reichen).
  • Du hast keine gemeinsamen Abfragen über Typgrenzen hinweg.

Best Practices

  • Halte Recording schlank: nur Metadaten und die polymorphe Referenz. Keine großen Textfelder.
  • Packe typenübergreifende Logik in Recording (oder Concerns), typenspezifische Logik in die Recordables.
  • Nutze delegate am Recording für gemeinsame Schnittstellen, z. B. delegate :export_html, to: :recordable.
  • Wenn Auditing/History wichtig ist, ergänze Events und schreibe Änderungen mit.
  • Autorisierung: Recording bündelt viele Aktionen – prüfe Berechtigungen konsequent.

Fazit

Das Recordable Pattern mit delegated_type ist eine elegante, praxiserprobte Lösung für polymorphe Inhalte in Rails. Es bringt dir:

  • eine einheitliche, performante Timeline,
  • klare Verantwortlichkeiten zwischen Metadaten und Inhaltsobjekten,
  • einfache Erweiterbarkeit ohne große Migrationen,
  • saubere APIs durch Delegation.

Wenn du Feed-ähnliche Strukturen, Versionierung und Kopier-Features brauchst, wirst du mit Recordings/Recordables sehr schnell produktiv.

Viel Spaß beim Ausprobieren – und schreib mir gern, wenn du Fragen hast oder Beispiele aus deinem Projekt teilen willst!

— Mathias (webmatze.de)

SUNO – AI Musik Generator getestet

Ich liebe es, neue Musik zu entdecken und zu hören. In der letzten Zeit bin ich dafür hauptsächlich auf Bandcamp unterwegs - einer Plattform, auf der viele, vor allem unbekanntere Künstler, ihr Schaffen teilweise sogar kostenlos anbieten. Wenn mir etwas gefällt, zögere ich nicht, es zu kaufen. Meine Liste an gekauften Alben wird bald die magische 200er-Grenze überschreiten.
https://bandcamp.com/webmatze

Wir alle wissen aber auch, dass es nicht leicht ist, als kreativer Kopf Aufmerksamkeit zu bekommen, geschweige denn davon leben zu können.

Und nun gibt es auch noch Konkurrenz aus einer ganz anderen Ecke für alle Kreativen. Die KI ist auf dem Weg, uns Menschen in kreativer Hinsicht den Rang abzulaufen.

Bilder erstellen? Für die KI kein Problem. Oder vielleicht gleich ganze Videos? Schafft die KI auch. Aber was ist mit Musik? Die kann doch nicht einfach generiert werden, oder?

Ich habe die letzen Tage einige Tools ausprobiert, die es uns erlauben mittels KI ganze Songs zu generieren. Vor allem SUNO hat mich überzeugt.

Es benötigt nur einen einfachen, kurzen Prompt, und schon wird ein kompletter Song innerhalb weniger Sekunden erzeugt. Und das Erschreckende ist, diese künstlich erzeugten Songs sind nicht einmal schlecht.

Seit Version 4 von SUNO kann man sie praktisch kaum noch von 'echten' Liedern unterscheiden.

Hier sind ein paar Beispiele, die ich zusammen mit meinen Kindern erstellt habe. Sie haben mir die Grundidee des Songs genannt, ich habe den Prompt geschrieben, und SUNO macht den Rest.

Ich schwebe:

Nacht der Schatten:

Mein Leben ist schön:

Oder auch auf englisch:

Was haltet ihr von KI-Song-Generatoren? Könnt ihr euch vorstellen, solche Musik zu hören? Würdet ihr überhaupt merken, dass ein Song künstlich erzeugt wurde?

Git Dashboard: Dein täglicher Überblick über Team-Aktivitäten

Hey Developer-Community!

Heute möchte ich euch ein cooles Tool vorstellen, das mir und meinem Team das Leben deutlich einfacher macht: Das Git Dashboard!

Was ist das Git Dashboard?

Stellt euch vor, ihr kommt morgens ins Büro und wollt schnell wissen:

  • Was habt ihr gestern alles committed?
  • Was haben eure Teammitglieder gemacht?
  • Wer war besonders aktiv?

Genau dafür haben wir das Git Dashboard entwickelt! Mit einem einzigen Befehl bekommt ihr einen übersichtlichen Report über alle Aktivitäten in eurem Repository.

Wie funktioniert's?

Super einfach! Nach der Installation könnt ihr zum Beispiel eingeben:

git dashboard              # Zeigt den Report von gestern
git dashboard -d today     # Zeigt den heutigen Report
git dashboard --date 2024-12-18  # Zeigt den Report für ein bestimmtes Datum

Was macht es so besonders?

  • Zeigt eure persönlichen Commits übersichtlich an
  • Listet die Commits eurer Teammitglieder auf
  • Erstellt eine Rangliste mit Commit-Zahlen und geänderten Zeilen
  • Berücksichtigt alle Branches
  • Flexibel im Datum - heute, gestern oder jedes andere Datum

Perfekt für Daily Standups!

Keine Panik mehr vor der Frage "Was hast du gestern gemacht?". Ein schnelles git dashboard und ihr habt alle Infos parat!

Probiert es aus!

Das Tool ist Open Source und super einfach zu installieren. Schaut euch das Projekt auf GitHub an und gebt ihm einen Stern, wenn es euch gefällt!

Installation in zwei Schritten:

git clone https://github.com/webmatze/git-dashboard.git
./install.sh

Viel Spaß beim Ausprobieren!

P.S.: Feedback und Contributions sind immer willkommen!