refactor: agents/ moves to ~/Projekte/ai-config

.zshrc sources agent-env.zsh from there; README points to the new home.
History of the moved files stays here up to e000cfe.

Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>
This commit is contained in:
mwiegand 2026-10-09 23:21:57 +02:00
parent e000cfe121
commit 450b58c2a5
No known key found for this signature in database
10 changed files with 5 additions and 648 deletions

2
.zshrc
View file

@ -41,4 +41,4 @@ bindkey '^[[B' down-line-or-beginning-search
bindkey '^[OA' up-line-or-beginning-search bindkey '^[OA' up-line-or-beginning-search
bindkey '^[OB' down-line-or-beginning-search bindkey '^[OB' down-line-or-beginning-search
source "${0:a:h}/agents/agent-env.zsh" source "$HOME/Projekte/ai-config/agents/agent-env.zsh"

View file

@ -6,9 +6,6 @@ Shared shell and tool configuration.
Tracked files: Tracked files:
- `.zshrc` — shared zsh UI behavior - `.zshrc` — shared zsh UI behavior
- `agents/AGENTS.md` — shared global personal preferences (agent-agnostic)
- `agents/AGENTS-claude.md` — Claude Code wrapper (`@AGENTS.md` + `@~/.claude/RTK.md`)
- `agents/AGENTS-opencode.md` — OpenCode wrapper (`@AGENTS.md`)
- `claude/statusline-command.sh` — Claude Code statusline (dir, branch, model, thinking, rate limits) - `claude/statusline-command.sh` — Claude Code statusline (dir, branch, model, thinking, rate limits)
This repo is intentionally small. Machine-specific setup stays local unless explicitly promoted into shared config later. This repo is intentionally small. Machine-specific setup stays local unless explicitly promoted into shared config later.
@ -21,15 +18,15 @@ This repo is intentionally small. Machine-specific setup stays local unless expl
source "$HOME/Projekte/dotfiles/.zshrc" source "$HOME/Projekte/dotfiles/.zshrc"
``` ```
**Claude Code & OpenCode**: symlink the wrappers into each agent's config dir: **Claude Code**: statusline symlink:
```zsh ```zsh
ln -s "$HOME/Projekte/dotfiles/agents/AGENTS-claude.md" ~/.claude/CLAUDE.md
ln -s "$HOME/Projekte/dotfiles/agents/AGENTS-opencode.md" ~/.config/opencode/AGENTS.md
ln -s "$HOME/Projekte/dotfiles/claude/statusline-command.sh" ~/.claude/statusline-command.sh ln -s "$HOME/Projekte/dotfiles/claude/statusline-command.sh" ~/.claude/statusline-command.sh
``` ```
The wrappers `@`-include the shared base `agents/AGENTS.md`; add agent-specific content directly in the wrapper file. Agent instructions, hooks, `.env.agents` and the agent SSH helpers moved to
`~/Projekte/ai-config/agents/` on 2026-10-09 (`.zshrc` sources `agent-env.zsh` from
there); see that folder's README for the wiring.
## What Lives Here ## What Lives Here

View file

@ -1,106 +0,0 @@
# Python schreibt sofort statt gepuffert: lange Läufe im Hintergrund lassen sich
# zwischendurch im Log mitlesen (sonst bleibt die Datei bis zum Ende leer)
export PYTHONUNBUFFERED=1
export USE_AGENT_SSH_USER=1
# fester Socket des geteilten Agent-ssh-agents (Start + Key-Load macht
# agent-env in ~/.zshrc) — hier gesetzt, damit Signing (GIT_CONFIG_* unten)
# und ssh auch dann auf dem Ein-Key-Agent landen, wenn eine Shell noch eine
# alte agent-env-Funktion ohne Socket-Export geladen hat
export SSH_AUTH_SOCK=/Users/mwiegand/.ssh/isac-agent.sock
export BW_SSH_ARGS="-o User=mwiegand-agent -o IdentitiesOnly=yes -i /Users/mwiegand/.ssh/id_mwiegand_agent.pub -o IdentityAgent=/Users/mwiegand/.ssh/isac-agent.sock"
export BW_SCP_ARGS="$BW_SSH_ARGS"
# Git-Signing in Agent-Sessions: derselbe Signing-Key wie interaktiv
# ("SSH Signing Key mwiegand"), über den geteilten Maschinen-Agent
# (isac-agent.sock via SSH_AUTH_SOCK). Signatur = "kam von meiner Maschine";
# wer committet hat, steht im Co-Authored-By-Trailer. GIT_CONFIG_* schlägt
# lokale und globale git-Configs — Overrides in Repos greifen hier nicht.
export GIT_CONFIG_COUNT=2
export GIT_CONFIG_KEY_0="user.signingkey"
export GIT_CONFIG_VALUE_0="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOxBXKEVpYdw4XBPKEzMDWJqvCy7YyogBaoAo2RnGXQ4"
export GIT_CONFIG_KEY_1="gpg.ssh.program"
export GIT_CONFIG_VALUE_1="ssh-keygen"
# influxdb
export INFLUXDB_READ_TOKEN="op://24ylu7osc22gtdcoic6rgjsp2m/mdrtosjm42btdrhejoo4eg4mmu/password"
export INFLUXDB_URL="op://24ylu7osc22gtdcoic6rgjsp2m/mdrtosjm42btdrhejoo4eg4mmu/url"
export INFLUXDB_ORG="op://24ylu7osc22gtdcoic6rgjsp2m/mdrtosjm42btdrhejoo4eg4mmu/org"
# bigquery
export BIGQUERY_SERVICE_ACCOUNT_JSON="op://24ylu7osc22gtdcoic6rgjsp2m/bjyiugpg4gzruvskxjita6eutm/martin-wiegand-ai-50f35be6b554.json"
export BIGQUERY_PROJECT_ID="op://24ylu7osc22gtdcoic6rgjsp2m/bjyiugpg4gzruvskxjita6eutm/project"
# agents guide
export AGENTS_GUID_TOKEN="op://24ylu7osc22gtdcoic6rgjsp2m/qzwjz73g2evbot5xqqnqtc7tiy/password"
# icinga
export ICINGA_BASE_URL="op://24ylu7osc22gtdcoic6rgjsp2m/stoktfjxvifslqwguhrpmprcwe/url"
export ICINGA_USER="op://24ylu7osc22gtdcoic6rgjsp2m/stoktfjxvifslqwguhrpmprcwe/username"
export ICINGA_API_PASSWORD="op://24ylu7osc22gtdcoic6rgjsp2m/stoktfjxvifslqwguhrpmprcwe/password"
# grafana
export GRAFANA_URL="op://24ylu7osc22gtdcoic6rgjsp2m/jwgq6zd77be4kwvk76q7lskc6i/url"
export GRAFANA_READ_TOKEN="op://24ylu7osc22gtdcoic6rgjsp2m/jwgq6zd77be4kwvk76q7lskc6i/password"
# ionos
export IONOS_API_TOKEN="op://24ylu7osc22gtdcoic6rgjsp2m/c67ygbr54xsxblxgnoljeawbse/password"
# swift
export SWIFT3_TOKEN="op://24ylu7osc22gtdcoic6rgjsp2m/uigw4g52gkd6ocmzeeqi4fk2wu/password"
# bitbucket dc
export BITBUCKET_DC_URL="op://24ylu7osc22gtdcoic6rgjsp2m/wipkwb7pmc2xyuq5vibywhqvpi/hostname"
export BITBUCKET_DC_PAT="op://24ylu7osc22gtdcoic6rgjsp2m/wipkwb7pmc2xyuq5vibywhqvpi/credential"
# bamboo
export BAMBOO_DC_URL="op://24ylu7osc22gtdcoic6rgjsp2m/delniouzyvkndwtkmovv7hhauy/hostname"
export BAMBOO_DC_PAT="op://24ylu7osc22gtdcoic6rgjsp2m/delniouzyvkndwtkmovv7hhauy/credential"
# atlassian cloud
export ATLASSIAN_CLOUD_PAT="op://24ylu7osc22gtdcoic6rgjsp2m/ywanqqqcviwqyoeoxa6g2au5ki/credential"
export JIRA_READONLY_PAT="op://24ylu7osc22gtdcoic6rgjsp2m/qqi25p2cuwkjkhbxj7spbjq2fe/credential"
# n8n prod
# "mcp": {
# "n8n-seibert": {
# "type": "local",
# "command": [
# "npx",
# "-y",
# "n8n-mcp"
# ],
# "environment": {
# "N8N_API_KEY": "{env:N8N_SEIBERT_API_KEY}",
# "N8N_API_URL": "{env:N8N_SEIBERT_API_URL}"
# }
# }
# }
export N8N_SEIBERT_API_URL="op://24ylu7osc22gtdcoic6rgjsp2m/qbkhrul2lm7uxy4kjqjpt5eoju/url"
export N8N_SEIBERT_API_KEY="op://24ylu7osc22gtdcoic6rgjsp2m/qbkhrul2lm7uxy4kjqjpt5eoju/credential"
# forgejo test token
export FORGEJO_URL="op://24ylu7osc22gtdcoic6rgjsp2m/zdkxvnabf23xjnp3lwti43hmma/hostname"
export FORGEJO_TOKEN="op://24ylu7osc22gtdcoic6rgjsp2m/zdkxvnabf23xjnp3lwti43hmma/credential"
# proxmox
export PROXMOX_URL_1="op://24ylu7osc22gtdcoic6rgjsp2m/xqpmcgcvytlmptfwwpn7kui6ei/url"
export PROXMOX_USER_1="op://24ylu7osc22gtdcoic6rgjsp2m/xqpmcgcvytlmptfwwpn7kui6ei/username"
export PROXMOX_TOKEN_1="op://24ylu7osc22gtdcoic6rgjsp2m/xqpmcgcvytlmptfwwpn7kui6ei/credential"
export PROXMOX_URL_2="op://24ylu7osc22gtdcoic6rgjsp2m/hiax7cmpeezanve2orhstjwchi/url"
export PROXMOX_USER_2="op://24ylu7osc22gtdcoic6rgjsp2m/hiax7cmpeezanve2orhstjwchi/username"
export PROXMOX_TOKEN_2="op://24ylu7osc22gtdcoic6rgjsp2m/hiax7cmpeezanve2orhstjwchi/credential"
# ai
export VLLM_API_KEY="op://24ylu7osc22gtdcoic6rgjsp2m/ryeegxxtakfolgqqgnwtdio2ny/credential"
export GOOGLE_AI_STUDIO_API_KEY="op://24ylu7osc22gtdcoic6rgjsp2m/e225knzbmb6rxe5utr2csjphqu/credential"
export CLAUDE_VERTEX_GATEWAY_TOKEN="op://24ylu7osc22gtdcoic6rgjsp2m/gwkninymrh2a7msvsou24c3zvu/credential"
export ONYX_ADMIN_TOKEN="op://24ylu7osc22gtdcoic6rgjsp2m/rw6ccrvzirrls47g4vkjurhzm4/credential"
# elewom
export ELEWOM_AUTH_TOKEN="op://24ylu7osc22gtdcoic6rgjsp2m/zwznfforzil5yrjrl77uuo67pi/credential"
# verda
export VERDA_INFERENCE_KEY="op://24ylu7osc22gtdcoic6rgjsp2m/sxnfz2logzqe47d4yilfh5whly/credential"
# seatable
export SEATABLE_ACCOUNT_TOKEN="op://24ylu7osc22gtdcoic6rgjsp2m/smfdkpo4ve7pfeabnoddls72hu/credential"
# bitbucket cloud seibert media workspace
export BITBUCKET_CLOUD_SEIBERT_WORKSPACE="op://24ylu7osc22gtdcoic6rgjsp2m/6vtqs6biiegyhi2z5i24u4nav4/credential"
# netbox
export NETBOX_AUTH_TOKEN="op://24ylu7osc22gtdcoic6rgjsp2m/5gzmvpudiuv6tdzi6amzsbkwcu/credential"
# sentry
export SENTRY_READ_TOKEN="op://24ylu7osc22gtdcoic6rgjsp2m/v3vxfekfbkenbz7hxaqjai4q4m/credential"

View file

@ -1,75 +0,0 @@
@./AGENTS.md
## Company knowledge (onyx & thicket)
- Company-knowledge questions go to the `onyx` skill first (its description
says when); its index surfaces pages that direct CQL/JQL misses. Always cite
source links.
- Two shortcuts past onyx: Thicket MCP for people lookups (role, team,
email — its other sections are thin), Confluence CQL for precise
lookups with known space/title.
- Same-topic follow-ups: use `onyx answer "..." --session <last session_id>`.
## Subagents
**Default to delegating whenever the expected tool output exceeds the
conclusion the main thread actually needs** — multi-file research, log/data
analysis, doc review, broad searches, verification runs. The session context
is the scarce resource; a subagent burns its own context and returns only the
result. Work inline only for single-fact lookups where file and symbol are
already known — there, a subagent is pure overhead.
Use the `Agent` tool. Prefer specialized agent types over `general-purpose`
when one fits:
- **`Explore`** — read-only search: *locates* code and facts (reads excerpts,
not whole files). It does not review or audit; quality judgments stay with
the parent or a dedicated reviewer.
- **`Plan`** — implementation strategy, architecture trade-offs.
- **`fork`** (`subagent_type: "fork"`) — inherits the full conversation
context, runs in the background, keeps its tool output out of the main
thread. The tool of choice for "do X with everything you already know, give
me only the outcome" — no need to re-explain state in the prompt.
- Agent types not listed as available in the session (e.g. a `code-reviewer`)
don't exist there — check the available-types list instead of guessing.
### Behavioral tier (MANDATORY — always set)
Prefix every `Agent` prompt with one of:
- `[TIER: FAST]` — return raw results immediately; no internal monologue, no synthesis
- `[TIER: STANDARD]` — focused implementation logic; minimal preamble
- `[TIER: DEEP]` — complex reasoning; must conclude with a summary
### Model selection (relative to the parent, no fixed names)
Resolution order: explicit `model` param → agent type frontmatter → parent
inheritance. (`fork` always runs on the parent model; an override is ignored.)
Model names shift (Opus was the top tier when this was written; Fable and
Mythos sit above it now), so think in tiers, not names:
0. **Self-hosted backend (a SessionStart hook says so when `ANTHROPIC_BASE_URL`
points at the vLLM gateway): never pass `model`.** Aliases do not exist
there; every subagent inherits the one available model.
1. **Specialized agent types** (`Explore`, `Plan`, …) keep their frontmatter
model. Leave `model` off.
2. **Routine `general-purpose` work** (mechanical edits, bulk lookups,
verification runs): one tier below the parent, today `sonnet`. Don't pay
top rates for routine work.
3. **Judgement work** (research, recommendation, trade-off, review, blinded
eval judges): the parent's tier, so pass no `model` and let it inherit.
For "how should X look" questions discuss options first, do not build.
4. **`haiku`** only for purely mechanical tasks: bulk classification, format
conversion, summarizing pre-filtered text. Code-semantic work is at least
`sonnet`.
### Context hygiene
- Prefer parallel `Explore` calls over sequential `general-purpose` sessions;
spawn independent agents in a single message so they run concurrently.
- Once delegated, don't duplicate: never run the same search yourself while
an agent is on it, and don't poll — results arrive as notifications.
- Follow-ups to an existing agent go via `SendMessage` (keeps its context);
a new `Agent` call always starts fresh — right for unrelated lookups.
- Summarize subagent findings in the main thread; the raw report is not
shown to the user, so relay what matters.

View file

@ -1 +0,0 @@
@./AGENTS.md

View file

@ -1,194 +0,0 @@
# Personal preferences
## Workflow
- **Specs, plans, and design docs go into the repo they describe.** When
brainstorming, plan-mode, or any flow produces a persistent document
(design spec, implementation plan, ADR, etc.), the final destination is
always the working tree of the project it belongs to — typically
under `docs/` (or wherever the project conventionally keeps them).
Scratch locations like `~/.claude/plans/<slug>.md` are fine *during*
plan mode, but the moment plan mode exits with an approved artifact,
copy it into the repo and `git commit` it. Don't ship a feature
while its spec or plan still lives only in a scratch file. If a
project's `AGENTS.md` / `CLAUDE.md` specifies a different directory,
that wins.
## Identity
- Email martin.wiegand@seibert.group, Bitbucket username `mwiegand`.
## Write actions: confirm before executing
Before any action that creates, modifies or sends something outside this
machine (Jira comments, ticket transitions, Confluence edits, chat messages,
emails), show exactly what will happen (the text, the target, the transition)
and wait for explicit approval. For bulk operations present the full list
first and change nothing until the plan is confirmed. Local edits, commits,
pushes and memory writes do not need this.
## External communication (messages, emails, comments)
- Neutral and technical: the agent conveys information, it does not imitate
a human. No social emojis, no emotional filler ("Danke dir! 🙂", "Huhu").
- Structure: factual salutation ("Hallo <Name>,"), precise content, concrete
call to action, closing without emotional markers.
- Append "(ai generated)" to every external text I phrase; never in chat
replies to me, and not in automated webhook or alert texts.
- PR thread replies carry only the decisive argument: no side findings,
links or follow-up ideas.
- Times in chat in Europe/Berlin local time; convert server UTC first.
## Error handling
If a tool call fails or hits a rate limit, pause and tell me. One retry with
the identified cause fixed (e.g. a sandbox denial) is fine; never retry in a
loop or brute-force through API calls.
## Git commits
- Conventional Commits; imperative, intent-focused subjects; body only when
needed; split unrelated changes; ask only for mixed diffs.
- On a Jira issue, prefix the subject with its key (`IT-12345`) in every repo.
The ISAC repo has its own single-line convention (`IT-12345 bw/<bundle>:
<what>`, no Conventional Commits); the `isac` skill has the details.
- **Push and open PRs (also drafts) without asking**, then report the link.
Not covered: force-push, pushes to shared or foreign branches, merges, PR
comments. Exception: in the private l4d2 project Martin pushes himself.
- **Never `--amend` and never `git stash pop` when other sessions may work in
the same repo.** An amend rewrote another session's pushed commit once
(2026-09-25); a pop after "No local changes to save" pops a foreign stash,
in the ISAC repo one with staged secrets. Fix-ups are new commits; to look
at an old version use `git show <ref>:<path>`.
- **Commit messages via `-F` use one absolute path for writing and reading**
(the Bash tool's cwd changes silently; a relative `.tmp/commitmsg.txt` once
landed in a task folder and the commit took the old message). Check
`head -1` before committing; several commits per turn get numbered files.
- **Own PR branches: readable over historical.** Re-cut commits into one per
logical step (pure `git mv` first, content second) and push with
`--force-with-lease`; rebuild from the final tree instead of amending.
Bitbucket detects renames only by content similarity, so keep a renamed
file small.
- `git push` over SSH fails inside the sandbox (no raw TCP; `excludedCommands`
does not help): push with `dangerouslyDisableSandbox: true`, ISAC via
`isac git -C <repo>`.
## Jira
- Search with JQL scoped to issues, never Confluence. Default filter:
`assignee = currentUser() AND status NOT IN (Done, Closed) ORDER BY duedate ASC`
- Asked to work on a ticket: present the ticket, then a numbered action plan.
No vague follow-up questions.
- Write actions (tickets, comments) go through the `jira-write` skill.
Comments are internal by default; public only when they address the
participants, never silently public.
## Code health
The `coding` skill carries the rules (act on cleanups, scope boundary, rule of
three, friction as architectural data, per-language style). Load it before
editing code if it did not trigger on its own.
## Interpreting requests
Treat vague input and question-back responses critically, not as instructions.
- **Vagueness is not authorization.** "Make it cleaner", "maybe X?",
"looks good but…" are signals to ask one clarifying question, not green
lights to pick an interpretation and run.
- **A tangent is not an answer.** If you ask "A or B?" and get "maybe X?",
that's a new question, not a pick. Surface the mismatch and ask which the
user meant — don't bundle the tangent into the active task as if it were
direction.
## Team memory (cognee)
cognee is a real memory, not an archive: unlike `MEMORY.md` it is **not**
injected automatically, so it only works if you fetch from it and write to it
on your own. Command forms, flags, dataset sharing and failure modes are in the
`cognee` plugin skill; this section only says *when*.
- **Read: `cognee recall <question>` before the first tool call** when the task
touches infrastructure, a server, a service, a ticket or a tool we run
ourselves; when an error "smells familiar"; when choosing between options
where a decision may already exist; or when the task will plausibly take more
than a handful of tool calls. Plain `recall` reads every readable dataset,
team included. Treat what comes back like a colleague's note: useful, possibly
outdated; verify a named file, flag or host still exists before acting on it.
- **Write: `cognee remember` as the insight appears**, not at session end.
Announce it in one line, never ask first: memory writes are exempt from any
project rule that asks to confirm write actions (those rules target external
systems: Jira, chat, Confluence, hosts). Moments: a bug fixed whose cause was
not visible in the code; a decision with a rejected alternative (save the
*why*); a measurement that contradicts an assumption; a correction, yours or
mine; a tool or API that behaves differently than its docs claim.
- **Routing:** `--team` for everything concerning team IT (Seibert IT-Infra,
ISAC/bw, monitoring, INFRA board, shared services, team processes), plain
`remember` for my preferences and personal context. Never secrets,
credentials or customer-personal data; nothing the repo already records;
nothing that only matters inside this conversation.
- **Projects with their own findings convention** (task folders,
`erkenntnisse/`, `decisions/`): write the file there first, then the cognee
note with the conclusion and a link to the file. The file is the detailed
record; cognee is what the next session or a colleague in another repo finds.
- **Hygiene:** on recurring topics `recall` first and extend the existing note
instead of adding a second one. Write notes that answer a question: concrete
numbers, the error message verbatim, the fix, and the external references
(Confluence, Jira, TeamVault, Google Chat thread URLs
`https://chat.google.com/room/<SPACE-ID>/<THREAD-ID>`). No size limit, so
write self-contained.
## Sandbox
- **Always run commands sandboxed first.** Only use `dangerouslyDisableSandbox: true` after a sandbox-related failure — never preemptively. One targeted retry with the identified cause fixed is fine; retrying in a loop is not.
- **On sandbox failure:** analyze the error message for the blocked path, then suggest adding it to `sandbox.filesystem.allowWrite` in `~/.claude/settings.json`. Only fall back to `dangerouslyDisableSandbox: true` if the path can't be determined or the user prefers it.
- **Commands that must run unsandboxed belong in `sandbox.excludedCommands`** (e.g. `git *`, the pinned
`bin/srv`-style wrappers), not behind the flag: the exclusion is the owner's decision, the flag is the
model's. Without a hook, the flag is only a convention: an unsandboxed command that matches an allow
rule (`Bash(git *)`, `Bash(pytest *)` …) runs without a prompt in default mode, and auto mode may wave it
through as well. (The former PreToolUse hook that forced a prompt on the flag was removed 2026-10-04;
its JSON is in this file's git history.)
## Temporary Files
- **Use `$TMPDIR` or `.tmp/` (project-local) for temporary files.** Never use `/tmp` directly or paths like `/Library/Application Support` — those are not in the sandbox allowlist and trigger permission prompts.
- `$TMPDIR` is set by the sandbox to a writable path. `.tmp/` inside the project directory is always writable without prompts.
- Ensure `.tmp/` is listed in `.gitignore` when creating temp files in a tracked repo.
## Shell — literal `!` in Bash commands
- **Never put a literal `!` in an inline Bash command.** Claude Code's Bash tool
escapes every `!` to `\!` at the transport layer before any shell sees it —
even inside single/double quotes and in non-interactive shells
(anthropics/claude-code#61121, a regression of a fix shipped in 2.1.87). The
stray backslash corrupts downstream tools: Python `!=` → SyntaxWarning + broken
string, jq, Jira JQL (`status != Done` → server 400), branch names, etc.
`bash -c '...'` and quoted heredocs do **not** avoid it.
- **One rule covers everything:** if a command needs a `!`, write the command/code
to a file with the Write tool under `.tmp/` and run the file. `.tmp/` is
prompt-free via the `Edit(.tmp/**)` allow rule in `~/.claude/settings.json`. Do
**not** create the file with inline `printf`/`echo >` — that re-escapes the `!`.
- Need a literal `!` inline anyway: ANSI-C hex `$'\x21'` (e.g. `B=$'\x21'; cmd
"${B}=…"`) yields a real `!` with no literal `!` in the command.
## Tooling
- **Sourcebot for Seibert repos without a local checkout:** the `sourcebot` skill
(MCP tools, gotchas such as mirrors and result caps; never `ask_codebase` on
your own). A local checkout stays first choice.
- **`ccc` before `grep` for concept questions.** In a repo with a
`.cocoindex_code/` directory, a question about a concept ("where is X
handled", "what talks to Y", "which code does Z") starts with
`ccc search '<concept>'` from the project root, then `grep` verifies what it
found. An exact string (UUID, ticket ID, error message, identifier) goes
straight to `grep`/`rg`: ccc is embeddings only and will not find it. Gotchas
in the `ccc` skill.
- **`ccc index` / `ccc init` are read-safe maintenance.** They only write to the
gitignored `.cocoindex_code/` directory, never to source. Run them without
confirmation prompts; treat like `grep` for permission purposes.
- **`md` for long Markdown files instead of `head`/`grep -n '^#'`.**
`md outline <file>` prints the heading tree with line numbers and size per
section; decide from that, then `md section <file> "<heading>"` for exactly
that subtree (`--range` yields `offset limit` for the Read tool, `--depth 0`
cuts subsections). It knows code fences and frontmatter, so a `# comment` in
a bash block is not a heading. Below ~500 lines just `Read` the file.

View file

@ -1,185 +0,0 @@
# Design: SSH-Key- und Git-Signing-Architektur (Arbeitsrechner mwiegand)
Stand 2026-08-04 — umgesetzt und verifiziert. Beschreibt das *Warum* hinter
dem Code in diesem Verzeichnis (`agent-env.zsh`, `ensure-machine-agent`,
`.env.agents`) plus `~/.ssh/config` und `~/.gitconfig`.
Umsetzungsstand, Historie und die offenen Schritte auf ISAC-Seite stehen im
ai-Repo: `tasks/2026-07-31-agent-key-isolation/HANDOFF.md`, Vorarbeit in
`tasks/2026-06-13-isac-infra-agent-isolation/`.
## Problem
Auf dem Rechner laufen zwei Arten von Akteuren mit unterschiedlichem
Vertrauensniveau:
- **Ich** (interaktive Shell) — brauche den persönlichen SSH-Key, der über
LDAP auch auf **Kundensystemen** autorisiert ist.
- **Agents** (Claude/OpenCode via `agent-env`) — sollen interne Systeme
erreichen, aber **strukturell nicht unbemerkt** an den kundenfähigen Key
kommen.
Vor dem Umbau war die Trennung rein konventioniert: der private Haupt-Key lag
unverschlüsselt als `~/.ssh/id_mwiegand` auf der Platte (für jede Agent-Session
lesbar), der 1Password-SSH-Agent bot alle Keys jedem Prozess an, und ein
Login-Script lud den Haupt-Key zusätzlich in den Apple-Agent. Git-Signing hing
am Datei-Key.
Dazu die Komfort-Anforderung: **Signieren darf im Alltag nicht nerven** —
keine Fingerabdruck-Freigabe pro Commit.
## Anforderungen
1. Agents verwenden einen eigenen SSH-Key (nur interne Systeme); der
kundenfähige Haupt-Key ist für sie nicht still nutzbar.
2. Jede Nutzung des Haupt-Keys erzeugt eine sichtbare, bewusste Freigabe
(Fingerabdruck) — der Prompt ist die Schranke.
3. Kein privater Schlüssel liegt als Datei auf der Platte.
4. Git-Commits sind signiert, prompt-frei — Herkunft „diese Maschine" ist
nachweisbar (Mensch/Agent unterscheiden die Commit-Trailer).
5. Kein Autoload beim Login (explizite Entscheidung): was in einem Agent
liegt, wurde bewusst dorthin gelegt.
## Architektur: drei Schlüssel, drei Wege
| Schlüssel (1Password-Item, Vault Employee) | Fingerprint | Lebt in | Zweck | Prompt-Verhalten |
|---|---|---|---|---|
| **SSH Key mwiegand** (Haupt-Key) | `SHA256:lIx1Na…` | nur 1Password | SSH überall, inkl. Kundensysteme | 1Password-Freigabe pro Key und Terminal-Session — bewusst spürbar |
| **SSH isac agent** (Agent-Key) | `SHA256:77FiJM…` | geteilter Maschinen-ssh-agent `~/.ssh/isac-agent.sock` | SSH auf interne Systeme (User `mwiegand-agent`) | einmal `op read` pro Boot, danach still |
| **SSH Signing Key mwiegand** (Signing-Key) | `SHA256:SvHSJQ…` | geteilter Maschinen-ssh-agent `~/.ssh/isac-agent.sock` | Git-Signing **aller** Commits der Maschine — meine und Agent-Commits | einmal `ensure-machine-agent` pro Boot (passiert automatisch mit der ersten Agent-Session), danach still |
Die Auswahl des Schlüssels erfolgt **über den Agent-Socket**, nicht über
Key-Dateien:
- `~/.ssh/config` `Host *` → `IdentityAgent ~/.1password/agent.sock`
(Haupt-Key, promptet pro Nutzung).
- `Match exec USE_AGENT_SSH_USER=1` → `User mwiegand-agent` +
`IdentityAgent ~/.ssh/isac-agent.sock` (Ein-Key-Agent; steht vor `Host *`,
nach den ckn-Host-Blöcken).
- Git-Signing läuft über `SSH_AUTH_SOCK`, der überall (interaktive Shell via
`agent-env.zsh`, Agent-Sessions zusätzlich via `.env.agents`) auf den
Maschinen-Agent zeigt — signiert wird immer mit dem Signing-Key. Fürs
interaktive SSH ist `SSH_AUTH_SOCK` irrelevant, weil die `ssh_config`
`IdentityAgent` explizit setzt.
Der Sicherheitskern: **die Prompt-Semantik bleibt sauber.** Ein
1Password-Prompt bedeutet immer „jemand will den kundenfähigen Key" — er
kommt selten und ist ernst zu nehmen. Signing und Agent-SSH erzeugen nie
Prompts, also gibt es keine Gewöhnung an reflexhaftes Bestätigen.
## Komponenten
Alles Agent-bezogene liegt versioniert in diesem Verzeichnis; `~/.zshrc` und
`~/.local/bin` enthalten nur noch Verweise darauf.
| Datei | Rolle |
|---|---|
| `agents/agent-env.zsh` (gesourced von `dotfiles/.zshrc`) | exportiert `SSH_AUTH_SOCK` fest auf `~/.ssh/isac-agent.sock`; `agent-env()` ruft `ensure-machine-agent` und dann `op run --env-file ~/.env.agents` |
| `agents/ensure-machine-agent` (Symlink in `~/.local/bin/`) | idempotent: startet den Maschinen-Agent bei Bedarf und stellt **beide** Keys sicher (Signing + Agent). Wird von `agent-env` gerufen; manuell nur nötig, wenn man nach einem Reboot committen will, bevor die erste Agent-Session lief |
| `agents/.env.agents` (Symlink `~/.env.agents`) | rein deklarativ — `op run` parst die Datei nur, führt keinen Code aus: `USE_AGENT_SSH_USER=1`, `SSH_AUTH_SOCK` (Robustheit: Konsumenten ohne `agent-env` finden den richtigen Socket), `BW_SSH_ARGS`, `GIT_CONFIG_*` → `user.signingkey` = Signing-Pubkey (Env schlägt alle git-Configs, auch lokale), Secret-Referenzen |
| `~/.ssh/config` | Socket-Routing s. o.; keine `IdentityFile`-Einträge für persönliche Keys |
| `~/.gitconfig` | `gpg.format=ssh`, `commit.gpgsign=true`, `user.signingkey` = Signing-Pubkey-String (kein Pfad), `gpg.ssh.allowedSignersFile` |
| `~/.ssh/allowed_signers` | beide Pubkeys (Signing + Agent) für lokale Verifikation (`git log --format=%G?`) |
Gelöscht: `~/.ssh/id_mwiegand` (unverschlüsselter Datei-Key),
`add-ssh-key-to-agent` (Login-Autoload des Haupt-Keys), lokale
`user.signingkey`-Overrides in 8 Repos.
## Entscheidungen und verworfene Alternativen
**Signing-Key als eigener Schlüssel statt Haupt-Key.** Die Prompt-Frequenz
hängt nicht am Schlüssel, sondern am Mechanismus — aber ein gemeinsamer
Schlüssel hätte Haupt-Key-Prompts beim Committen alltäglich gemacht und die
Schranke durch Gewöhnung entwertet. Außerdem: Forges kennen den Signing-Key
nur als Signaturschlüssel; ein Leak ist kein Systemzugriff.
**Ein gemeinsamer Signing-Key für Mensch und Agents** (statt Agent-Commits
mit dem Agent-SSH-Key zu signieren, wie zuerst gebaut). Die Key-Trennung
hätte nur *ehrliche* Agent-Commits von meinen unterschieden: die
Agent-Sockets sind für jeden lokalen Prozess erreichbar, ein böswilliger
Agent könnte ohnehin als ich signieren. Für ehrliche Commits leistet der
`Co-Authored-By`-Trailer die Unterscheidung. Die Signatur bedeutet ehrlich
„kam von meiner Maschine"; dafür halbiert sich die Forge-Pflege (ein Key
registrieren/verifizieren statt zwei) und der Boot-Load läuft automatisch
über die erste Agent-Session mit. Ältere Commits (2026-08-04) sind noch mit
dem Agent-Key signiert — er bleibt dafür in `allowed_signers`.
**`op-ssh-sign` erwogen und verworfen.** Zuerst umgesetzt (Key verlässt
1Password nie), dann zurückgebaut: die 1Password-Autorisierung gilt global
pro App(+Terminal-Session) für *alle* Keys. Das Signing-Nerv-Problem hätte
sich nur durch Lockern des Modells lösen lassen — was die Haupt-Key-Schranke
mitgelockert hätte. Der Agent-Weg entkoppelt beides: Signing prompt-frei,
1Password-Modell bleibt streng. (Zwischenschritt Apple-Agent als
Signing-Halter wurde durch den Maschinen-Agent ersetzt — ein Agent weniger.)
**macOS-Keychain als Signer verworfen.** Die Keychain speichert Bytes, kann
aber keine SSH-Signaturen erzeugen (`security` bietet nur `cms`). Jeder
Keychain-Weg endet in einer Klartext-Tempdatei pro Commit — schlechter als
jeder Agent. (`ssh-add --apple-use-keychain` speichert nur Passphrasen und
setzt eine Key-Datei voraus.)
**Kein Login-Autoload.** Bewusste Entscheidung: Keys werden nicht beim Login
geladen, sondern lazy — mit der ersten Agent-Session (automatisch) bzw. per
`ensure-machine-agent` von Hand. Symptom eines fehlenden Loads ist ein
Commit-Fehler `Couldn't find key in agent?` — eindeutig und folgenlos.
**Geteilter Agent statt Agent pro Session.** Ein fester Socket
(`~/.ssh/isac-agent.sock`) statt mktemp pro Session: ein `op read` pro Key
und Boot statt pro Session, kein Cleanup verwaister Agents, und der
Match-Block kann auf einen stabilen Pfad zeigen (wirkt auch für Subprozesse
mit bereinigtem Env). Beide Bewohner (Agent-Key, Signing-Key) sind per
Definition die Schlüssel, die Agents zugänglich sein dürfen — Langlebigkeit
weicht nichts auf.
**`SSH_AUTH_SOCK` zusätzlich fest in `.env.agents`.** Lektion aus dem Umbau:
eine Shell mit veralteter `agent-env`-Funktion lud die neue `.env.agents`
(Signing-Config auf Agent-Key), zeigte aber noch auf den leeren Apple-Agent →
`Couldn't find key in agent?`. Signing-Config und Socket dürfen nicht
auseinanderfallen; beide stehen jetzt in derselben Datei.
## Grenzen (bewusst offen)
Die Architektur ist eine **Zustimmungs- und Erkennungsschranke, keine
technische Unerreichbarkeit**:
- Der 1Password-Socket bleibt für Agent-Prozesse erreichbar
(`allowAllUnixSockets: true` in der Claude-Sandbox-Config; 1Password kann
Keys nicht pro Client filtern). Ein Agent, der den Haupt-Key anfordert,
löst aber einen sichtbaren Prompt aus, den ich ablehnen kann.
- **`op`-CLI-Pfad** (empirisch geprüft 2026-08-04): ein Agent kann `op read`
auf beliebige Items *versuchen* — u. a. den Haupt-Key — aber nicht
unbemerkt. Vier Versuche aus einer Agent-Session ergaben vier
1Password-Dialoge; die Freigabe für das `op run` beim `agent-env`-Start
deckt nachgelagerte `op`-Aufrufe **nicht** mit ab (ob eine *bestätigte*
Freigabe danach ein 10-Minuten-Fenster öffnet, ist unverifiziert — der
Dialog behauptet das, gemessen wurde es nicht).
Die verbleibende Schwäche ist die **Blindheit des Dialogs**: er sagt „full
access to your account", nennt aber nicht das angefragte Item. Beim
Bestätigen ist nicht unterscheidbar, ob ein Agent ein harmloses
API-Passwort oder den kundenfähigen SSH-Key liest. Das ist systematisch,
nicht nur eine Eigenschaft offener Abfragen: auch `op read` mit einer
punktgenauen `op://Employee/<item>/<feld>`-Referenz zeigt denselben
Dialog (geprüft 2026-08-04) — 1Password autorisiert das Werkzeug, nie den
Zugriff, selbst wenn dieser vollständig spezifiziert ist. Gerade die
Secrets in `.env.agents` sind ausschließlich solche Referenzen — und weil legitime
Agent-Starts denselben Dialog erzeugen, trainiert der Alltag reflexhaftes
Bestätigen. Genau das macht der gescopte Service-Account (Schritt 1)
überflüssig: was nicht anfragbar ist, muss nicht beurteilt werden.
- Vollständige Schließung = HANDOFF-Schritte 1/3: gescopter Service-Account
mit eigenem Vault für den Agent-Key, Socket-Sperre in der Sandbox,
Seatbelt-Variante für unsandboxte Kommandos.
## Betrieb
- **Nach Reboot:** die erste `agent-env`-Session lädt beide Keys automatisch;
nur wer vorher committen will, ruft einmal `ensure-machine-agent` (Symptom
sonst: `Couldn't find key in agent?`).
- **Verifikation:** `git log --format='%h %G? %GK'` — `G` + `SvHSJQ…` für
alle Maschinen-Commits (Commits vom 2026-08-04 noch mit `77FiJM…`).
`ssh-add -l` in einer Agent-Session zeigt genau die zwei Maschinen-Keys.
- **Forges:** der Signing-Pubkey muss bei Bitbucket DC/Forgejo als *Signing*
Key registriert sein; bei Forgejo zusätzlich ownership-verifiziert
(Challenge signieren, nur via Web-UI), sonst „unverified" (offen).
- **Rotation:** neuer Key = neues 1Password-Item + Pubkey in
`~/.gitconfig`/`allowed_signers`/`.env.agents` bzw. ISAC
(`admin_agent_users`) nachziehen; es gibt keine Key-Dateien zu tauschen.

View file

@ -1,18 +0,0 @@
# Agent-Sessions (Claude, OpenCode): Secrets aus 1Password und SSH-Identität
# des Agent-Users. Wird von dotfiles/.zshrc gesourced.
# Architektur und Begründungen: agents/README.md.
# Git-Signing und ssh-add greifen über SSH_AUTH_SOCK auf den geteilten
# Maschinen-Agent zu — interaktiv wie in Agent-Sessions. Fürs interaktive SSH
# ist die Variable irrelevant, weil ~/.ssh/config IdentityAgent explizit setzt
# (Haupt-Key via 1Password, Agent-User via diesem Socket).
export SSH_AUTH_SOCK="$HOME/.ssh/isac-agent.sock"
agent-env() {
# Agent + beide Maschinen-Keys sicherstellen; der op-read darin teilt sich
# die 1Password-Autorisierung mit dem op-run unten, also ein Prompt.
ensure-machine-agent || return 1
op run --account WHZOJQOJPZHTFPJC6JZAOCUCBI \
--env-file ~/.env.agents --no-masking -- "$@"
}

View file

@ -1,11 +0,0 @@
#!/bin/sh
# SessionStart hook: tell the agent when the session runs against the self-hosted
# vLLM gateway, where model aliases (sonnet, opus, haiku) do not exist.
# Prints nothing on Anthropic/Vertex so the default rules apply.
case "${ANTHROPIC_BASE_URL:-}" in
*vllm*|*seibert.tools*)
model="${ANTHROPIC_MODEL:-${CLAUDE_CODE_SUBAGENT_MODEL:-the configured model}}"
printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"Self-hosted backend (%s): only %s is available. Never pass a model override to Agent calls; all subagents inherit this model."}}\n' "$ANTHROPIC_BASE_URL" "$model"
;;
esac
exit 0

View file

@ -1,50 +0,0 @@
#!/bin/sh
# Stellt den geteilten Maschinen-ssh-agent sicher: Socket läuft und hält beide
# Keys, die auf dieser Maschine gebraucht werden. Idempotent — mehrfach
# aufrufbar, macht nur was fehlt.
#
# SSH isac agent SSH auf interne Systeme (User mwiegand-agent)
# SSH Signing Key Git-Signing, für mich UND Agent-Sessions
#
# Wird von agent-env bei jedem Start gerufen; manuell nur nötig, wenn man nach
# einem Reboot committen will, bevor die erste Agent-Session lief (Symptom:
# "error: Couldn't find key in agent?").
#
# Der kundenfähige Haupt-Key ("SSH Key mwiegand") gehört bewusst NICHT hierher:
# er lebt nur hinter der 1Password-Freigabe (~/.1password/agent.sock, siehe
# Host * in ~/.ssh/config), damit Agents ihn nicht unbemerkt nutzen können.
# Architektur und Begründungen: agents/README.md im dotfiles-Repo.
PATH="/opt/homebrew/bin:/usr/bin:/bin:$PATH"
SOCK="$HOME/.ssh/isac-agent.sock"
OP_ACCOUNT="WHZOJQOJPZHTFPJC6JZAOCUCBI"
export SSH_AUTH_SOCK="$SOCK"
# Agent tot oder nie gestartet? (ssh-add exit 2 = Socket antwortet nicht)
ssh-add -l >/dev/null 2>&1
if [ $? -eq 2 ]; then
rm -f "$SOCK"
/usr/bin/ssh-agent -a "$SOCK" >/dev/null || exit 1
fi
# $1 = Fingerprint (schon geladen?), $2 = op-Referenz, $3 = Anzeigename
ensure_key() {
if ssh-add -l 2>/dev/null | grep -q "$1"; then
return 0
fi
if op read --account "$OP_ACCOUNT" "$2" | ssh-add -q -; then
echo "$3 geladen (bis zum nächsten Reboot)." >&2
else
echo "$3 konnte nicht geladen werden (1Password gesperrt?)" >&2
return 1
fi
}
rc=0
ensure_key "SHA256:SvHSJQcHlAWUmF8EnflNsrv0mhWGARvAKLRPSBQ460I" \
"op://Employee/37z2tbpilrjrgyrhckb5cbrhsq/private key?ssh-format=openssh" \
"Signing-Key" || rc=1
ensure_key "SHA256:77FiJM6Z1eN7Ka7YpoCKIn9xVJFQUj6nBQa/g1rFya4" \
"op://Employee/ec2mtn4kkxnypyc4gxutg7lajq/private key?ssh-format=openssh" \
"Agent-Key" || rc=1
exit $rc