Edit files with the djinnvim CLI — vim-style, pattern-anchored keyhole editing that never reads whole files. Use for any file editing when djinnvim is installed, especially when file/shell tools are restricted or files are large.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add anschnapp/djinnvim --skill skill --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Skill?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/anschnapp-skill)More formats (shields.io, HTML) on the badges page.
---
name: djinnvim
description: Edit files with the djinnvim CLI — vim-style, pattern-anchored keyhole editing that never reads whole files. Use for any file editing when djinnvim is installed, especially when file/shell tools are restricted or files are large.
---
# djinnvim: keyhole editing from the shell
You edit through small viewports, the way a vim user does: search by
pattern, look at the echo, edit. Never read a whole file — every command
returns the few lines around what it did, and that echo IS your
verification (no re-reads needed).
Reach for it when finding or changing something would otherwise mean
reading a whole file, when a change repeats across many sites, or when
file and shell tools are restricted. Creating a file or rewriting one
wholesale is not its job.
## Setup — every command, two rules
1. **Pin the sandbox root in the same shell command** (shell env does not
persist between your calls):
```
export DJINNVIM_ROOTS=/abs/path/to/project; djinnvim open src/app.py
```
2. **Pass each editor command as ONE quoted argument.** Single-quote it;
switch to double quotes only when the text itself contains a single
quote:
```
djinnvim edit 'at /old_name/ ciw new_name'
djinnvim substitute ":%s/'eu-west'/'us-east'/"
```
3. **`-f PATH` names the file to work on**, opening it if needed, so
`edit`, `substitute`, `print`, `matches` and `write` need no separate
`open` call: `djinnvim edit -f src/app.py 'at /old/ ciw new'`. Without
it they use the active buffer. A switch is announced in the echo
(`[now on ... ]`).
State (open buffers, cursor, registers, undo) persists across your shell
calls via an auto-spawned per-session daemon — nothing to start or manage.
`djinnvim status` shows it, `djinnvim shutdown` stops it (unwritten buffers
die with it; only `write` touches disk). Exit codes: 1 = the editor said
`error: ...` (read it — the buffer is untouched), 2 = usage/daemon problem.
## The seven verbs
- `djinnvim open PATH` — open/switch the active buffer and show its head.
Optional, given `-f` above; use it when you want the file's metadata.
Relative paths resolve against the root, not your cwd.
- `djinnvim motion CMD` — move the cursor, one motion per call:
`/pattern` (regex, forward), `?pattern` (back), `n`/`N` (next/prev —
n is ALWAYS forward, N ALWAYS backward, unlike vim), `:80` (line),
`gg`/`G`, `fx`/`Fx` (char on the cursor line). Search is strictly after
the cursor and wraps, reporting `match i of n (wrapped)`.
- `djinnvim matches PATTERN [-C 1]` — grep-style listing of every match
(capped at 50). **Call this before any rename-like edit** to see all
sites and decoys.
- `djinnvim edit CMD` — vim normal-mode edit (details below).
- `djinnvim substitute CMD` — ex command (details below).
- `djinnvim print [CMD]` — read-only window print (ed/vim `:p`), the
reading tool: `'p'` (current line, cursor stays), `':80 p'` /
`':/def load/ p'` (cursor MOVES there, prints it), `':10,25 p'`
(range; cursor to its last line). Widen with `above`/`below`/`around`
+ `tiny` (8) / `middle` (25) / `long` (50) or a number —
`':/def load/ p around middle'`; `around` counts EACH side. Max ~100
lines per call; page by re-addressing a gutter line number.
- `djinnvim write` — save the active buffer; reports lines changed.
`write --preview` shows the full pending buffer-vs-disk ±diff without
writing — the final review before committing.
## edit
Anchored form (preferred): `at /pattern/ <cmd>` (regex) or
`at "literal text" <cmd>` (matched literally — no escaping, use it when
the anchor has parens, dots or other regex punctuation; it cannot contain
a double quote). Ordinal and offset work on both: `at 2nd /pat/ <cmd>`.
**The anchor lands at the START of the match — anchor on the
exact text to change:** `at /15\)/ ciw 60` changes the 15 in
`retries(15)`; `at /retries=15/ ciw 60` would change `retries`.
A `+N`/`-N` after the closing slash moves the anchor N whole lines
(cursor at column 0): `at /# Merge logic/-1 O text` inserts above the
line ABOVE the match — e.g. above a comment banner the match sits inside.
`at each /pattern/ <cmd>` applies one edit command at EVERY match
(transactional: any failure changes nothing; one undo step; returns a
±diff). Text objects make it structural: `at each /# obsolete/ dap`
deletes every marked paragraph whole — no line counting. To go
match-by-match instead, reissue the same `at /pat/ <cmd>`; it anchors on
the NEXT match each time.
Commands: `ciw`/`caw TEXT`, `ci(`/`{`/`[`/`"`/`' TEXT` (`di`/`da`
delete), `dip`/`dap` (paragraph, delete only), `dd`, `cc TEXT`, `D`,
`C TEXT`, `x`, `r<char>`, `o`/`O TEXT` (line below/above; multi-line OK),
`A`/`I TEXT` (line end/start), `i`/`a TEXT` (before/after the cursor
char), `cs"'` / `ds"` / `ysiw"` (surround). Changes need TEXT, deletes
take none; everything after the first space is TEXT, verbatim. Newlines
in TEXT must be **real newline characters** (one Enter each, vim-exact) —
the two characters backslash-n are not translated, they stay as typed, so
source like `print("a\nb")` inserts correctly. (This is the opposite of
`substitute`, where `\n` in the *replacement* does produce a newline.) `o`/`O` are line-wise — to
insert below a multi-line statement, anchor on its LAST line, not its
first. `dd` deletes the cursor line; address it by pattern
(`at /pattern/ dd`) — `edit` takes no ex addresses.
**Indentation is vim autoindent.** The line-wise inserts `o`, `O` and `cc`
take the reference line's indent and your TEXT's own leading whitespace
stacks on top, so pass only the indent BEYOND the anchor's: `o x = 1`
after a 4-space line lands at 6 spaces, and `cc members = sorted(raw)`
inside an 8-space block stays at 8. `o!`/`O!`/`cc!` opt out and insert
TEXT literally, for a block that is already absolutely indented.
(`substitute` replacements are the other way round — always literal, so
capture the indent there with `^( +)` and `\1`.) `o`/`O` also echo
pre-edit blank-line counts next to where they landed (`2 blank line(s)
above insertion point, 0 below`) — read it to match a file's blank-line
convention (e.g. 2 lines between top-level defs) without counting from
the viewport.
Blank lines are ordinary edits, one call each: bare `o`/`O` with no TEXT
inserts exactly one empty line, and `at /pattern/ dd` on a blank line
removes one. Don't reach for `substitute` (`:/pat/s/^/\n/`, `:N,Nd`) to
fix spacing — take the counts from the `o`/`O` echo and correct with a
single anchored call.
Registers: `yy` / `y<i|a><obj>` yank, `p`/`P` paste. `"name` prefix
composes with the anchor: `at /def helper/ "fn dap` cuts the function,
`"fn p` pastes it (works across files). Only "name-prefixed deletes write
registers; a wrong name on `p` lists them all.
`u` undoes the last buffer change (repeat to go further; crosses writes;
no redo). Any bad echo → `u` reverts it whole.
## substitute
Ex forms: `:%s/old/new/g` (file), `:s/old/new/` (cursor line),
`:10,40s/foo/bar/`, `:/start/,/end/s/x/y/g`, `:g/pat/d` (delete matching
lines). Flags `g`, `i`. **Regex and replacement are Python `re` syntax**
(`\1` groups): escape parens in the PATTERN (`send_request\(x\)`) but
write them plainly in the replacement. Both range addresses are
inclusive; any address takes `+N`/`-N` — end on `/pat/-1` for "up to but
not including". Zero matches is a loud error, never a silent no-op.
Numeric addresses go stale after every edit; prefer pattern addresses.
To rewrite one line into several without retyping its indentation,
capture it: `:s/^( +)old_tail/\1new\n\1 second line/` (`\n` in the
replacement inserts a line break). Replacements are always literal here —
this capture is only needed in `substitute`; `edit`'s `o`/`O`/`cc` supply
the indent for you.
Line-shaped only: to remove whole blocks at every match use
`edit 'at each /pat/ dap'`, not hand-counted ranges. Register ranges for
blocks text objects can't grab (function with internal blank lines):
`:/def helper/,/^def /-1d fn`, then paste with `at /def target/ "fn P`.
## Workflow
1. `open` the file (echo shows size — you never need more than that).
2. `matches` the symbols you'll touch: counts expose decoys
(`fetch_records` vs `fetch_records_cached` → use `\b...\b`) and plan
scoping before any edit.
3. Edit smallest-first tool: one-site → anchored `edit`; many-line regex
→ `substitute`; many-site structural → `at each`.
4. **Read every echo.** The diff/viewport is the verification; a wrong
echo → `edit u` immediately. (The cursor line's `→ ` prefix is exactly
as wide as other lines' two-space prefix — indentation shown is exact.
Where the caret appears it also states the line's indentation relative
to the line above — read that instead of counting leading spaces.)
5. `write --preview` for a final buffer-vs-disk diff, then `write`, and
check the reported changed-line count against what you expect.
6. **Write before running anything against the file.** Tests, linters,
and file reads see only the disk — never unwritten buffer state. (A
disk change under an open buffer fails loudly on the next edit/write;
re-open to continue.)
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!