MyFamilyTracker
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
A complete walkthrough of building a production Claude Code plugin — from plugin.json manifest to skill design to hook integration. Using DroidForge (Android build automation) as the real example, with the actual code and design decisions.
On this page
I've published 4 Claude Code plugins. The most complex is DroidForge — an Android build workflow engine that handles signing, versioning, Gradle DSL generation, and MVVM scaffolding across my 22-app portfolio.
Building it taught me how Claude Code's plugin system actually works, what makes skills effective vs. useless, and where the sharp edges are. Here's the full picture.
A plugin is a directory with a
plugin.jsonThe directory structure:
DroidForge/
├── plugin.json # Manifest — name, version, skills declared
├── skills/
│ ├── SKILL.md # Auto-discovery index (required)
│ ├── new-android-project.md
│ ├── signing-setup.md
│ ├── version-bump.md
│ ├── gradle-dsl.md
│ └── scaffold-screen.md
├── hooks/
│ └── security-scan.py
└── README.md{
"name": "DroidForge",
"version": "1.2.0",
"description": "Android build workflow engine — signing, versioning, Gradle DSL, MVVM scaffolding",
"author": "SUDARSHANCHAUDHARI",
"skills": [
"skills/new-android-project.md",
"skills/signing-setup.md",
"skills/version-bump.md",
"skills/gradle-dsl.md",
"skills/scaffold-screen.md"
],
"hooks": [
{
"event": "PreToolUse",
"automation": ["Write", "Edit", "MultiEdit"],
"command": "python3 hooks/security-scan.py"
}
]
}Key points:
skillshooksPreToolUseThe most common mistake in skill writing is describing what to do instead of encoding how to do it. A skill that says "create a new Android screen following MVVM" is useless. A skill that contains the exact code templates is how you get consistent output.
---
name: scaffold-screen
description: Scaffold a new Jetpack Compose screen with ViewModel, sealed UiState,
and Hilt injection. Use when adding any new screen to an Android app.
---
# Scaffold Screen
## When to use
When the user says "add a [name] screen" or "create a new screen for [feature]".
## Output
Create these files:
### 1. Screen Composable
Path: `presentation/ui/screen/[feature]/[Feature]Screen.kt`
```kotlin
@Composable
fun [Feature]Screen(
viewModel: [Feature]ViewModel = hiltViewModel(),
onNavigateBack: () -> Unit
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
[Feature]Content(
uiState = uiState,
onAction = viewModel::onAction,
onNavigateBack = onNavigateBack
)
}
@Composable
private fun [Feature]Content(
uiState: [Feature]UiState,
onAction: ([Feature]Action) -> Unit,
onNavigateBack: () -> Unit
) {
when (uiState) {
is [Feature]UiState.Loading -> LoadingScreen()
is [Feature]UiState.Success -> { /* content */ }
is [Feature]UiState.Error -> ErrorScreen(
message = uiState.message,
onRetry = { onAction([Feature]Action.Retry) }
)
}
}Path:
presentation/viewmodel/[Feature]ViewModel.kt@HiltViewModel
class [Feature]ViewModel @Inject constructor(
private val repository: [Feature]Repository
) : ViewModel() {
private val _uiState = MutableStateFlow<[Feature]UiState>([Feature]UiState.Loading)
val uiState: StateFlow<[Feature]UiState> = _uiState.asStateFlow()
fun onAction(action: [Feature]Action) {
when (action) {
is [Feature]Action.Retry -> load()
}
}
private fun load() {
viewModelScope.launch {
_uiState.value = [Feature]UiState.Loading
repository.get()
.onSuccess { _uiState.value = [Feature]UiState.Success(it) }
.onFailure { _uiState.value = [Feature]UiState.Error(it.message ?: "Error") }
}
}
}sealed interface [Feature]UiState {
data object Loading : [Feature]UiState
data class Success(val data: [Feature]Data) : [Feature]UiState
data class Error(val message: String) : [Feature]UiState
}
sealed interface [Feature]Action {
data object Retry : [Feature]Action
}After scaffolding:
@HiltViewModelcollectAsStateWithLifecycle()collectAsState()when
Three things make this skill work:
1. **Exact code templates** — not prose descriptions, not pseudocode. Real Kotlin with `[Feature]` as the only placeholder.
2. **Verification checklist** — Claude checks its own output against concrete criteria.
3. **Narrow trigger condition** — "when the user says 'add a screen'" prevents the skill from activating at wrong times.
---
## The Hook: Security Credential Scanner
Every file write in my sessions goes through this hook:
```python
#!/usr/bin/env python3
# hooks/security-scan.py
import sys
import json
import re
PATTERNS = [
(r'storePassword\s*=\s*["\'][^"\']{4,}["\']', "Keystore password"),
(r'keyPassword\s*=\s*["\'][^"\']{4,}["\']', "Key password"),
(r'api[_-]?key\s*[:=]\s*["\'][^"\']{8,}["\']', "API key"),
(r'-----BEGIN.*PRIVATE KEY-----', "Private key"),
(r'AAAA[0-9A-Za-z_-]{20,}', "Possible FCM/push token"),
]
def scan(content: str) -> list[str]:
found = []
for pattern, label in PATTERNS:
if re.search(pattern, content, re.IGNORECASE | re.MULTILINE):
found.append(label)
return found
def main():
try:
data = json.load(sys.stdin)
except json.JSONDecodeError:
# Malformed input — allow and let Claude handle
print(json.dumps({"decision": "allow"}))
return
# Only scan file write content
content = data.get("content", "") or data.get("new_string", "")
issues = scan(content)
if issues:
print(json.dumps({
"decision": "block",
"reason": f"Potential credential detected: {', '.join(issues)}. Review before writing to disk."
}))
else:
print(json.dumps({"decision": "allow"}))
main()The hook receives tool call parameters as JSON via stdin and outputs a decision JSON.
"decision": "block""decision": "allow"This has caught real mistakes: generated signing config templates with placeholder passwords that matched credential patterns, hardcoded test tokens in generated test files, and once a legitimate API key that slipped into a generated config.
The version bump skill is more complex because it needs to coordinate changes across multiple files and validate the result:
---
name: version-bump
description: Bump app version in config/version.properties and update versionCode.
Use when releasing or preparing a release candidate.
---
# Version Bump
## Steps
1. Read `config/version.properties`:VERSION_MAJOR=1 VERSION_MINOR=3 VERSION_PATCH=2 VERSION_CODE=47
2. Apply the bump based on user's argument:
- `patch` → VERSION_PATCH +1, VERSION_CODE +1
- `minor` → VERSION_MINOR +1, VERSION_PATCH=0, VERSION_CODE +1
- `major` → VERSION_MAJOR +1, VERSION_MINOR=0, VERSION_PATCH=0, VERSION_CODE +1
3. Write the updated properties file.
4. Verify `build.gradle.kts` reads from version.properties (look for
`val versionProps = Properties()`). If it doesn't, flag this as an issue.
5. Output the new version for confirmation:Version bumped: 1.3.2 (47) → 1.3.3 (48)
## Common issues
- `VERSION_CODE` must always increment, even for patch bumps — Play Store rejects downgrades
- Never edit versionCode or versionName directly in build.gradle.kts — always via version.propertiesThe skill works because it has the exact file format, the exact transform logic, and the verification step. Claude doesn't need to figure any of this out — it just executes the documented process.
First version mistake: skills too generic.
My initial
android-conventions.mdBetter approach: one skill per specific, triggerable task.
new-android-project.mdscaffold-screen.mdsigning-setup.mdSecond mistake: no verification steps.
Skills without verification steps produce output that looks correct but has subtle errors — missing annotations, wrong import paths, non-exhaustive when expressions. Adding verification checklists means Claude checks its own output before handing it back.
Third mistake: inconsistent placeholder format.
I used
FeatureNamefeature_name{feature}[Feature]# Install locally for development
claude plugin install ./DroidForge --local
# Install from GitHub (published plugins)
claude plugin install github:SUDARSHANCHAUDHARI/DroidForge
# List installed plugins
claude plugin list
# Update a plugin
claude plugin update DroidForgeFor local development,
--localAfter 6 months, DroidForge runs in every Android session without me thinking about it. New screens are scaffolded in 30 seconds. Version bumps are one command. Signing configs are generated correctly the first time.
The accumulated time savings are significant. But the real value is consistency: every app in my portfolio uses identical patterns because the skill enforces them. When I look at app 22's code, it's the same structure as app 1's. That's only possible because a documented process runs every time.
That's the real point of a plugin: not just automation, but encoded institutional knowledge that runs in every session without manual recall.
Sudarshan Chaudhari
AI Systems Builder / Product Engineer
Bangkok, Thailand
Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.
Related Posts
Related Apps
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
Building something? Available for Android dev and QA consulting.
Work with meComments — powered by Giscus
KMP credential vault and release state manager — Kotlin Multiplatform shared module for signing configs, version state, and Play Store metadata across 22+ Android projects.
ReadMulti-repo git assistant — scans all local repositories for uncommitted changes, analyzes diffs, and generates structured atomic commit messages. Android companion included.
Read