Skip to content

Commit 3aeaf52

Browse files
committed
Deploy: linux-sysadmin skill enforcement + keepass-cred-mgr REPL hardening
2 parents 90ad900 + 7170f5f commit 3aeaf52

254 files changed

Lines changed: 36743 additions & 749 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude-plugin/marketplace.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@
5252
},
5353
{
5454
"name": "linux-sysadmin",
55-
"description": "Linux system administration skills: 97 per-service guides covering daemons, CLI tools, and filesystems with annotated configs, cheatsheets, and a guided /sysadmin stack design workflow",
56-
"version": "1.1.0",
55+
"description": "Linux system administration skills: 137 per-service guides covering daemons, CLI tools, and filesystems with annotated configs, cheatsheets, and a guided /sysadmin stack design workflow",
56+
"version": "1.2.0",
5757
"author": {
5858
"name": "L3DigitalNet",
5959
"url": "https://github.com/L3DigitalNet"
@@ -130,7 +130,7 @@
130130
{
131131
"name": "keepass-cred-mgr",
132132
"description": "MCP server for secure KeePass vault access from Claude Code via YubiKey authentication. Exposes 10 tools for vault unlock, listing, searching, reading, writing, and bulk-importing KeePass entries with audit logging.",
133-
"version": "0.5.1",
133+
"version": "0.5.2",
134134
"author": {
135135
"name": "L3DigitalNet",
136136
"url": "https://github.com/L3DigitalNet"

.serena/project.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,12 @@ initial_prompt: ""
126126
# This overrides the corresponding setting in the global configuration; see the documentation there.
127127
# If null or missing, use the setting from the global configuration.
128128
symbol_info_budget:
129+
130+
# line ending convention to use when writing source files.
131+
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
132+
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
133+
line_ending:
134+
135+
# list of regex patterns which, when matched, mark a memory entry as read‑only.
136+
# Extends the list from the global configuration, merging the two lists.
137+
read_only_memory_patterns: []

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Claude-Code-Plugins/
3131
│ ├── design-assistant/ # Design document authoring and review
3232
│ ├── github-repo-manager/ # Conversational GitHub repo maintenance
3333
│ ├── home-assistant-dev/ # HA integration dev toolkit + MCP server
34-
│ ├── linux-sysadmin/ # Linux sysadmin skills (94 service, tool, and filesystem guides)
34+
│ ├── linux-sysadmin/ # Linux sysadmin skills (137 service, tool, and filesystem guides)
3535
│ ├── plugin-review/ # Plugin quality review via orchestrator
3636
│ ├── plugin-test-harness/ # Iterative test/fix/reload loop (TypeScript)
3737
│ └── release-pipeline/ # Autonomous release pipeline

docs/skills.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,92 @@ Run YAML validator:
458458
python -c "import yaml; yaml.safe_load(open('skill.md').read().split('---')[1])"
459459
```
460460

461+
## Linux Sysadmin Skill Conventions
462+
463+
The `linux-sysadmin` plugin follows stricter conventions than general skills. These ensure
464+
consistency across 100+ service, tool, and filesystem guides.
465+
466+
### Identity Format Convention
467+
468+
Choose the format based on whether the tool runs as a persistent process:
469+
470+
**Services/daemons** (anything with a systemd unit or persistent process) use bullet-list format:
471+
472+
```markdown
473+
## Identity
474+
- **Unit**: `nginx.service`
475+
- **Config**: `/etc/nginx/nginx.conf`
476+
- **Logs**: `journalctl -u nginx`, `/var/log/nginx/access.log`
477+
- **Install**: `apt install nginx` / `dnf install nginx`
478+
```
479+
480+
**CLI tools** (stateless, no persistent process) use property table format:
481+
482+
```markdown
483+
## Identity
484+
485+
| Property | Value |
486+
|----------|-------|
487+
| **Binary** | `jq` |
488+
| **Config** | No persistent config |
489+
| **Type** | CLI tool |
490+
| **Install** | `apt install jq` / `dnf install jq` |
491+
```
492+
493+
### Required Sections (in order)
494+
495+
1. **Identity** — binary/unit, config paths, logs, install command
496+
2. **Quick Start** — 3-5 shell commands from zero to working (fenced bash block)
497+
3. **Key Operations** — task/command table
498+
4. **Expected Ports** — (services with network listeners only)
499+
5. **Health Checks** — (services with observable state only)
500+
6. **Common Failures** — symptom/cause/fix table
501+
7. **Pain Points** — bullet list of gotchas and non-obvious behavior
502+
8. **See Also** — related skills with one-line descriptions
503+
9. **References** — pointer to `references/` directory
504+
505+
### Column Header Standards
506+
507+
Standardize table headers for machine-parseable consistency:
508+
509+
- Key Operations: `| Task | Command |`
510+
- Common Failures: `| Symptom | Cause | Fix |`
511+
512+
If a skill uses bullet-list format for Key Operations (e.g., nginx), that is acceptable — do
513+
not force a table where bullets work better.
514+
515+
### Frontmatter Fields
516+
517+
```yaml
518+
name: string # Required. Unique lowercase-hyphenated identifier
519+
description: string # Required. Human-readable summary (no trigger phrases)
520+
triggerPhrases: # Recommended. Array of activation phrases
521+
- "nginx"
522+
- "reverse proxy"
523+
globs: # Optional. File patterns for context matching
524+
- "**/nginx.conf"
525+
last_verified: "YYYY-MM" # Recommended. Date of last doc verification, or "unverified"
526+
```
527+
528+
Keep trigger phrases OUT of the description field. The description should read as a clean
529+
one-line summary; trigger phrases go in the `triggerPhrases` array.
530+
531+
### Reference Files
532+
533+
Each skill directory contains a `references/` subdirectory with:
534+
535+
- **`docs.md`** — Official and community documentation links (required)
536+
- **`cheatsheet.md`** or **`common-patterns.md`** — Practical command/config examples
537+
- **`*.annotated`** — Annotated config files with every option explained (for services with config files)
538+
539+
### Cross-References (See Also)
540+
541+
Every skill should list 2-5 related skills in its `## See Also` section. Group by relationship:
542+
543+
- **Alternatives** — tools that solve the same problem differently (nginx ↔ caddy)
544+
- **Complements** — tools commonly used together (prometheus → grafana)
545+
- **Dependencies** — tools this one sits on top of (helm → kubernetes)
546+
461547
## Next steps
462548

463549
- [Create plugins](./plugins.md) to package and distribute skills

plugins/keepass-cred-mgr/.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "keepass-cred-mgr",
3-
"version": "0.5.1",
3+
"version": "0.5.2",
44
"description": "MCP server for secure KeePass vault access from Claude Code via YubiKey authentication",
55
"author": {
66
"name": "L3DigitalNet",

plugins/keepass-cred-mgr/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66

7+
## [0.5.2] - 2026-03-18
8+
9+
### Fixed
10+
- Newlines in REPL arguments (notes, urls) corrupt the keepassxc-cli command stream, causing garbled entries and data loss. `_repl_quote()` now sanitizes `\n`/`\r` to spaces before quoting.
11+
- `deactivate_entry` constructed notes with an embedded newline (`\n[DEACTIVATED: ...]`) that split the REPL command across two lines. Uses ` | ` separator instead.
12+
- `run_cli` stdin_lines (passwords) now reject embedded newlines with a clear error instead of silently corrupting the stream.
13+
- Corrected REPL quoting model: the CLI uses `Utils::splitCommandString` (backslash escapes any character), not `QProcess::splitCommand`. Updated comments and docs.
14+
- `_parse_show_output` no longer misinterprets notes containing `Password: ...` or `URL: ...` text as field boundaries. Notes is the last standard field; only `Tags:` terminates it.
15+
- Notes passed via `--notes` are now escaped to prevent keepassxc-cli from silently converting literal `\n` text into newlines (the CLI replaces `\\n` → newline before storing).
16+
717
## [0.5.1] - 2026-03-15
818

919
### Changed

plugins/keepass-cred-mgr/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ flowchart TD
9393
Poller -.->|removed > grace period| AutoLock[Auto-lock vault]
9494
```
9595

96-
The server runs as a stdio MCP process spawned by Claude Code via `scripts/start-server.sh`, which resolves Python dependencies through `uv run` and starts the FastMCP server. On startup it loads the YAML config, initializes the YubiKey poller, and registers all 10 tools. The vault starts locked; call `unlock_vault` first to verify YubiKey presence and perform a physical touch. `unlock_vault` opens a persistent `keepassxc-cli open` REPL process; that single touch covers all subsequent tool calls in the session. Commands are dispatched through the REPL's stdin/stdout with Qt-style double-quote argument escaping; the REPL stays alive until the vault locks. Removal of the YubiKey starts a grace timer (default 10 seconds); if the key isn't reinserted in time, the vault locks (killing the REPL process), and all subsequent tool calls fail with `VaultLocked` until `unlock_vault` is called again.
96+
The server runs as a stdio MCP process spawned by Claude Code via `scripts/start-server.sh`, which resolves Python dependencies through `uv run` and starts the FastMCP server. On startup it loads the YAML config, initializes the YubiKey poller, and registers all 10 tools. The vault starts locked; call `unlock_vault` first to verify YubiKey presence and perform a physical touch. `unlock_vault` opens a persistent `keepassxc-cli open` REPL process; that single touch covers all subsequent tool calls in the session. Commands are dispatched through the REPL's stdin/stdout with double-quote argument escaping matching keepassxc-cli's `Utils::splitCommandString` parser (backslash escapes any character, double quotes toggle quoting mode); the REPL stays alive until the vault locks. Removal of the YubiKey starts a grace timer (default 10 seconds); if the key isn't reinserted in time, the vault locks (killing the REPL process), and all subsequent tool calls fail with `VaultLocked` until `unlock_vault` is called again.
9797

9898
## Usage
9999

@@ -178,7 +178,7 @@ audit_log_path: ~/.local/share/keepass-cred-mgr/audit.jsonl
178178

179179
- **Tag-based access control over group allowlist**: Earlier versions required an `allowed_groups` allowlist. This was replaced with a denylist of two KeePassXC tags: `AI RESTRICTED` (blocks all AI access to an entry) and `READ ONLY` (blocks write operations). Tags are parsed from `keepassxc-cli show` output during each tool call — no config field required. The inversion from opt-in allowlist to opt-out denylist means the user can freely add, remove, or reorganize groups without reconfiguring the plugin.
180180

181-
- **`keepassxc-cli` over `pykeepass`**: Using the CLI means the MCP server has no direct database access; KeePassXC owns the file format, locking, and YubiKey integration. `unlock_vault` opens a persistent `keepassxc-cli open` REPL process; all subsequent commands are dispatched through that process's stdin/stdout without re-authenticating. `list_entries` still issues one `ls` plus one `show` per entry (for metadata), but all within a single session rather than spawning a subprocess per call. Binary attachment exports use a separate subprocess since raw bytes cannot pass through the text REPL without corruption.
181+
- **`keepassxc-cli` over `pykeepass`**: Using the CLI means the MCP server has no direct database access; KeePassXC owns the file format, locking, and YubiKey integration. `unlock_vault` opens a persistent `keepassxc-cli open` REPL process; all subsequent commands are dispatched through that process's stdin/stdout without re-authenticating. `list_entries` still issues one `ls` plus one `show` per entry (for metadata), but all within a single session rather than spawning a subprocess per call. Binary attachment exports use a separate subprocess since raw bytes cannot pass through the text REPL without corruption. The REPL uses `Utils::splitCommandString` for argument parsing (backslash-escapes-any-character semantics, double-quote toggling); `_repl_quote()` matches this model.
182182

183183
- **`ykman list` for presence polling**: `keepassxc-cli` requires a physical touch on every invocation. Using `ykman list` (pure USB enumeration, no touch) allows continuous polling without interrupting the user.
184184

@@ -200,6 +200,7 @@ audit_log_path: ~/.local/share/keepass-cred-mgr/audit.jsonl
200200
- **No entry deletion or overwrite**: By design, Claude cannot delete or overwrite entries. Credential rotation requires a create-then-deactivate sequence, and stale `[INACTIVE]` entries accumulate until manually removed in KeePassXC.
201201
- **Titles with slashes are unsupported**: `keepassxc-cli` uses `/` as a path separator (`Group/Title`). Titles containing `/` produce undefined CLI behavior; `create_entry` rejects them with an error.
202202
- **`edit --notes` replaces the entire field**: Appending a deactivation timestamp to notes requires reading the existing notes first, then writing the combined string. If the notes update fails after a successful rename, the entry is still deactivated (renamed to `[INACTIVE]`) but the deactivation timestamp in notes may be missing. A warning is logged in this case.
203+
- **REPL is line-based**: The keepassxc-cli REPL reads one command per line. Arguments containing literal newlines are sanitized to spaces before sending (with a warning logged). Multi-line notes are flattened; the `--notes` `\\n`-to-newline conversion is pre-escaped to prevent silent data corruption.
203204

204205
## Security Model
205206

plugins/keepass-cred-mgr/server/tools/read.py

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,20 @@
2323
type SearchResult = dict[str, str | None]
2424

2525

26-
# Known field prefixes that terminate a multi-line notes block.
27-
_KNOWN_FIELDS = {"username", "password", "url", "notes", "title", "tags"}
26+
# Fields that appear BEFORE notes in keepassxc-cli show output.
27+
# Once we enter notes mode, only "tags" (which appears after notes) terminates it.
28+
# This prevents notes containing "Password: ..." from being misinterpreted as fields.
29+
_PRE_NOTES_FIELDS = {"username", "password", "url", "title"}
2830

2931

3032
def _parse_show_output(stdout: str) -> EntryFields:
3133
"""Parse keepassxc-cli show output into a string field dict.
3234
33-
Notes can span multiple lines; continuation lines have no 'Key: ' prefix.
34-
Continuation stops when a line starts with a known field key followed by ': '.
35+
keepassxc-cli show outputs fields in order: Title, UserName, Password,
36+
URL, Notes, then Tags. Notes can span multiple lines with embedded
37+
newlines. Once we enter notes mode, only a Tags line terminates it;
38+
lines that look like "Password: foo" inside notes are continuation lines,
39+
not field boundaries.
3540
"""
3641
fields: dict[str, str] = {}
3742
in_notes = False
@@ -41,22 +46,24 @@ def _parse_show_output(stdout: str) -> EntryFields:
4146
if ": " in line:
4247
key, _, value = line.partition(": ")
4348
key_lower = key.strip().lower()
44-
if key_lower in _KNOWN_FIELDS:
45-
if in_notes:
49+
50+
# Inside notes, only "tags" can terminate — everything else is content
51+
if in_notes:
52+
if key_lower == "tags":
4653
fields["notes"] = "\n".join(notes_lines)
4754
in_notes = False
48-
if key_lower == "notes":
49-
notes_lines = [value.strip()]
50-
in_notes = True
51-
elif key_lower == "username":
52-
fields["username"] = value.strip()
53-
elif key_lower == "password":
54-
fields["password"] = value.strip()
55-
elif key_lower == "url":
56-
fields["url"] = value.strip()
57-
elif key_lower == "title":
58-
fields["title"] = value.strip()
55+
# Tags value is captured by _parse_tags separately; skip
56+
continue
57+
notes_lines.append(line)
5958
continue
59+
60+
if key_lower == "notes":
61+
notes_lines = [value.strip()]
62+
in_notes = True
63+
elif key_lower in _PRE_NOTES_FIELDS:
64+
fields[key_lower] = value.strip()
65+
continue
66+
6067
if in_notes:
6168
notes_lines.append(line)
6269

plugins/keepass-cred-mgr/server/tools/write.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,18 @@
3838
log: structlog.stdlib.BoundLogger = structlog.get_logger("keepass-cred-mgr.tools.write")
3939

4040

41+
def _escape_notes_for_cli(notes: str) -> str:
42+
"""Escape a notes string for --notes argument to keepassxc-cli.
43+
44+
The CLI's Add.cpp and Edit.cpp replace literal '\\n' with actual newlines
45+
before storing (entry->setNotes(notes.replace("\\\\n", "\\n"))). If the
46+
user's notes contain the two-character sequence '\\n', the CLI silently
47+
converts it to a newline. Escape it to '\\\\n' so the CLI round-trips
48+
to '\\n' as intended.
49+
"""
50+
return notes.replace("\\n", "\\\\n")
51+
52+
4153
@contextmanager
4254
def _write_lock(vault: Vault) -> Generator[FileLock]:
4355
"""Acquire and release a file lock around database writes."""
@@ -104,7 +116,7 @@ async def create_entry(
104116
if url is not None:
105117
cmd.extend(["--url", url])
106118
if notes is not None:
107-
cmd.extend(["--notes", notes])
119+
cmd.extend(["--notes", _escape_notes_for_cli(notes)])
108120
if password:
109121
# keepassxc-cli add has no --password flag; -p prompts stdin.
110122
# Write the password to stdin upfront so run_cli() doesn't deadlock.
@@ -142,7 +154,8 @@ async def deactivate_entry(
142154
break
143155

144156
timestamp = datetime.now(UTC).isoformat()
145-
new_notes = f"{existing_notes}\n[DEACTIVATED: {timestamp}]".strip()
157+
deactivated_tag = f"[DEACTIVATED: {timestamp}]"
158+
new_notes = f"{existing_notes} | {deactivated_tag}".strip(" |") if existing_notes else deactivated_tag
146159
new_title = f"{INACTIVE_PREFIX}{title}"
147160

148161
with _write_lock(vault):
@@ -151,7 +164,7 @@ async def deactivate_entry(
151164
# Update notes using new path — non-critical, log and continue on failure
152165
new_path = vault.entry_path(new_title, group)
153166
try:
154-
await vault.run_cli("edit", "--notes", new_notes, db, new_path)
167+
await vault.run_cli("edit", "--notes", _escape_notes_for_cli(new_notes), db, new_path)
155168
except KeePassCLIError:
156169
log.warning("deactivate_notes_update_failed", title=title, group=group)
157170

0 commit comments

Comments
 (0)