app
Create, modify, and manage shizuku app configurations. Use when the user wants to create a new app, add template files to an app, or modify existing app structure.
SKILL.md.claude/skills/app/Skill.md
GitHubShizuku App Manager
This skill helps create and manage shizuku application configurations.
What This Skill Does
When invoked, this skill helps you:
- Scaffold new apps - Create complete directory structure and registration
- Add template files - Create and manage .tmpl files in app contents
- Modify existing apps - Update app configurations and template data
- Add installer logic - Implement Install() method for Homebrew packages
App Structure Overview
Apps live under apps/ in one of three category directories:
apps/languages/— language toolchains (e.g.golang,rust)apps/programs/— regular programs (e.g.helix,kitty)apps/agents/— agentic coding tools (e.g.claude)
Each shizuku app follows this pattern:
apps/{category}/{appName}/
├── {appName}.go # App struct with methods
└── contents/ # Template files and assets (optional, embedded via //go:embed)
├── file.conf # Regular files (copied as-is)
└── file.tmpl # Go templates (processed)
Modern App Architecture
Apps use an interface-based design with optional capabilities:
Core Interface (Required)
All apps must implement the App interface:
Name() string- Returns the app nameEnabled(cfg *config.Config) bool- Checks if app is enabled
Optional Interfaces
Apps can optionally implement:
FileSyncer- For apps that sync configuration filesSync(outDir string, cfg *config.Config) error
FileGenerator- For diff support without syncing side effectsGenerate(outDir string, cfg *config.Config) (*app.GenerateResult, error)
Installer- For apps that need installationInstall(cfg *config.Config) error
EnvProvider- For apps that provide environment variables/aliasesEnv() (*app.EnvSetup, error)
AgentConfigProvider- For apps that need agentic tools (LSP plugins, sandbox hosts/paths) to support themAgentConfig() app.AgentConfig
Creating a New App
Step 1: Gather Requirements
Ask the user:
- Category - Which category does this app belong to?
languages/- language toolchains (e.g. golang, rust)programs/- regular programs (e.g. helix, kitty)agents/- agentic coding tools (e.g. claude)
- App name - What is the name of the application? (e.g., "glow", "alacritty")
- Default enabled - Should this app be enabled by default? (most apps: true, optional services: false)
- Installation - Does the app need installation via Homebrew? (y/n)
- If yes: package name, cask vs formula, any taps needed?
- File syncing - Does the app need to sync configuration files? (y/n)
- If yes: destination path (e.g., "~/.config/glow/")
- Template data - Does the app need any template variables? (y/n)
- Remote files - Does the app need to download any remote resources like plugins? (y/n)
- Environment - Does the app need environment variables or aliases? (y/n)
- Agent config - Does the app need agentic tools to install LSP plugins, allow specific sandbox hosts, or write to specific paths? (y/n)
Step 2: Create Directory Structure
mkdir -p apps/{category}/{appName}
# Only create contents/ if the app will sync files
mkdir -p apps/{category}/{appName}/contents
Step 3: Create the Go File
Create apps/{category}/{appName}/{appName}.go with the appropriate template:
Minimal App (No File Syncing)
package {appName}
import (
"github.com/eleonorayaya/shizuku/config"
)
type App struct{}
func New() *App {
return &App{}
}
func (a *App) Name() string {
return "{appName}"
}
func (a *App) Enabled(cfg *config.Config) bool {
return cfg.GetAppConfigBool(a.Name(), "enabled", {defaultEnabled})
}
App with Installation
package {appName}
import (
"fmt"
"github.com/eleonorayaya/shizuku/config"
"github.com/eleonorayaya/shizuku/util"
)
type App struct{}
func New() *App {
return &App{}
}
func (a *App) Name() string {
return "{appName}"
}
func (a *App) Enabled(cfg *config.Config) bool {
return cfg.GetAppConfigBool(a.Name(), "enabled", {defaultEnabled})
}
func (a *App) Install(cfg *config.Config) error {
// For Homebrew formula
if err := util.InstallBrewPackage("{packageName}"); err != nil {
return fmt.Errorf("failed to install {packageName}: %w", err)
}
// For Homebrew cask
// if err := util.InstallCask("{caskName}"); err != nil {
// return fmt.Errorf("failed to install {caskName}: %w", err)
// }
// For custom tap
// if err := util.AddTap("{tapName}"); err != nil {
// return fmt.Errorf("failed to add tap: %w", err)
// }
return nil
}
App with File Syncing
package {appName}
import (
"fmt"
"github.com/eleonorayaya/shizuku/app"
"github.com/eleonorayaya/shizuku/config"
)
type App struct{}
func New() *App {
return &App{}
}
func (a *App) Name() string {
return "{appName}"
}
func (a *App) Enabled(cfg *config.Config) bool {
return cfg.GetAppConfigBool(a.Name(), "enabled", {defaultEnabled})
}
//go:embed all:contents
var contents embed.FS
func (a *App) Sync(outDir string, cfg *config.Config) error {
data := map[string]any{}
fileMap, err := app.GenerateAppFiles("{appName}", contents, data, outDir)
if err != nil {
return fmt.Errorf("failed to generate app files: %w", err)
}
if err := app.SyncAppFiles(fileMap, "{destinationPath}"); err != nil {
return fmt.Errorf("failed to sync app files: %w", err)
}
return nil
}
App with Remote Files
func (a *App) Sync(outDir string, cfg *config.Config) error {
data := map[string]any{}
fileMap, err := app.GenerateAppFiles("{appName}", contents, data, outDir)
if err != nil {
return fmt.Errorf("failed to generate app files: %w", err)
}
remoteFiles := map[string]string{
"plugins/file.wasm": "https://example.com/file.wasm",
}
pluginMap, err := app.FetchRemoteAppFiles(outDir, "{appName}", remoteFiles)
if err != nil {
return fmt.Errorf("failed to fetch remote files: %w", err)
}
maps.Copy(fileMap, pluginMap)
if err := app.SyncAppFiles(fileMap, "{destinationPath}"); err != nil {
return fmt.Errorf("failed to sync app files: %w", err)
}
return nil
}
Note: If using maps.Copy, add import: "maps"
App with Environment Variables
func (a *App) Env() (*app.EnvSetup, error) {
return &app.EnvSetup{
Variables: []app.EnvVar{
{Key: "EDITOR", Value: "hx"},
},
Aliases: []app.Alias{
{Name: "ls", Command: "lsd"},
},
}, nil
}
Step 4: Register the App
- Open the consumer binary at
examples/eleonora/main.go - Add import:
"github.com/eleonorayaya/shizuku/apps/{category}/{appName}" - Add the app to the matching
AddLanguages/AddPrograms/AddAgentcall innewBuilder():{appName}.New(),
IMPORTANT: Apps are registered in the consumer binary via the shizuku Builder — not in a central registry. The shizuku library itself does not know about any specific apps.
Step 4b: Add AgentConfig (optional)
If the app needs agentic tools to install LSP plugins, allow specific sandbox domains, or write to specific paths, implement AgentConfigProvider:
func (a *App) AgentConfig() app.AgentConfig {
return app.AgentConfig{
Plugins: []string{"my-lsp@claude-plugins-official"},
SandboxAllowedDomains: []string{"my-package-registry.example"},
SandboxAllowWrite: []string{"~/.cache/my-tool"},
}
}
The sync orchestrator collects this from every enabled app and passes it to agents (e.g. claude) via a SyncContext. No agent code needs to change when a new app declares its requirements.
Step 5: Build and Test
task build
task run -- list # Verify app appears
task run -- install # Test installation (if implemented)
task run -- sync # Test file syncing (if implemented)
Configuration System
Checking if App is Enabled
All apps implement Enabled() which checks the config:
func (a *App) Enabled(cfg *config.Config) bool {
return cfg.GetAppConfigBool(a.Name(), "enabled", true) // default: true
}
- Most apps default to
true(core functionality) - Optional services default to
false(protonpass, protonvpn, sfsymbols)
User Config Example
Users can control apps in ~/.config/shizuku/shizuku.yml:
apps:
helix:
enabled: false
terraform:
enabled: false
protonpass:
enabled: true
Using Other Config Values
Use cfg.GetAppConfigBool() for boolean values:
useNerdFont := cfg.GetAppConfigBool(a.Name(), "nerd_font", true)
Use cfg.GetAppConfig() for other types:
theme, ok := cfg.GetAppConfig(a.Name(), "theme")
if ok {
themeStr, ok := theme.(string)
if ok {
data["Theme"] = themeStr
}
}
Adding Template Files
When the user wants to add configuration files to an app:
- Ask for file path - Relative to contents/ directory (e.g., "config.conf")
- Ask if it's a template - Does it need variable substitution? (y/n)
- Create the file - If template, use
.tmplextension - Update template data - If template, add necessary variables to the
datamap in the Sync method
Template Example
If creating contents/config.conf.tmpl:
# {{ .AppName }} Configuration
theme = "{{ .Theme }}"
Update the Sync method:
data := map[string]any{
"AppName": "MyApp",
"Theme": "monade",
}
Modifying Existing Apps
When modifying an app:
- Read the current Go file to understand existing structure
- List contents/ to see existing files (if applicable)
- Make requested changes while preserving the app pattern
- Ensure all required interfaces are still implemented
Important Notes
- No comments - Don't add comments unless explicitly requested (per CLAUDE.md)
- Error wrapping - Always use
fmt.Errorf("context: %w", err)pattern - Template extension -
.tmplfiles are processed, extension is removed in output - File syncing -
SyncAppFileshandles directory creation and file copying - App struct - Always use
type App struct{}withNew() *Appconstructor - Interface implementation - Implement only the interfaces the app needs
- Registration - Apps are registered in the consumer binary (
examples/eleonora/main.go) viaAddLanguages/AddPrograms/AddAgenton the Builder, not in a central registry
Installation Utilities
Available Homebrew utilities in util/homebrew.go:
InstallBrewPackage(packageName string) error- Install formula (auto-checks if exists)InstallCask(caskName string) error- Install cask (auto-checks if exists)AddTap(tapName string) error- Add Homebrew tap (auto-checks if exists)BrewPackageExists(packageName string) (bool, error)- Check if package is installed
All utilities are idempotent - safe to call multiple times.
Workflow
- Use TodoWrite to track the scaffolding steps
- Ask clarifying questions using AskUserQuestion (always ask the category first)
- Create all necessary files in
apps/{category}/{appName}/ - Register the app in
examples/eleonora/main.gounder the matching Builder call - Suggest running
task buildandtask run -- listto verify
Examples
Simple App (Installation Only)
- App: rust
- Default enabled: true
- Installation:
InstallBrewPackage("rustup") - No file syncing
App with File Syncing
- App: helix
- Default enabled: true
- Installation:
pkg.Install(pkg.Spec{Brew: "helix", ...}) - Syncs to: ~/.config/helix/
- Provides: Environment variables (EDITOR=hx, VISUAL=hx)
Optional Service App
- App: protonpass
- Default enabled: false
- Installation:
InstallCask("proton-pass") - No file syncing
App with Templates
- App: sketchybar
- Default enabled: true
- Installation: Tap + formula
- Syncs to: ~/.config/sketchybar/
- Templates: Uses {{ .Theme }} and other variables
App with Remote Files
- App: terminal
- Default enabled: true
- Installation:
pkg.Installfor zsh and oh-my-posh - Syncs to: ~/.config/ohmyposh/
- Remote: Clones the zsh-vi-mode plugin from GitHub
App with Custom Installer
- App: kitty
- Default enabled: true
- Installation: Uses curl installation script (not Homebrew)
- Syncs to: ~/.config/kitty/