설치
npm install dsh-checkout-guard이 명령은 GitHub 저장소 주소에서 생성됩니다. 실행 전에 업스트림 README와 소스를 검토하고 재현성이 필요하면 release 또는 commit을 고정하세요.
README
유지 관리자가 작성한 문서 스냅샷입니다.
dsh-checkout-guard
Is the working copy you are about to write to the one you think it is?
A DSH plugin that answers that before you edit, commit, or push — not after. It reads a git checkout and tells you where it sits relative to its remote, how stale that answer is, who your next commit will be attributed to, what work is already sitting in the index, and whether another clone of the same repository on this machine has moved ahead of the one you are in.
Read-only. Offline unless you ask for a fetch. Never takes the index lock.
Why
Every check here exists because a specific mistake actually happened, usually to an agent working across many repositories at once:
| What goes wrong | What you see afterwards |
|---|---|
| You edit a checkout that is 8 commits behind | Your "fix" reverts work, or your push is rejected after the fact |
| Two clones of one repository, you are in the stale one | Changes vanish; the repo on the forge never shows them |
| A concurrent process moved HEAD to another branch | Your commit lands on a branch you have never heard of |
Something was already staged before you ran git add | Your commit packages somebody else's work in progress |
No repository-local user.email | A work address is permanently attached to a public commit |
| Detached HEAD | The commits exist, then don't, and nothing warned you |
None of these throw. Git does exactly what you asked; you asked in a state you had misread. That is the entire problem space this plugin covers.
Install
npm install dsh-checkout-guard
Then append to your profile's cordis.yml:
- id: checkout-guard
name: 'dsh-checkout-guard'
Node 22.19+ or 24+. The only runtime requirement is git on PATH.
Tools
checkout_guard — one working copy, in depth
{ "path": "/abs/path/to/repo", "expectBranch": "main" }
{
"branch": { "current": "main", "detached": false, "upstream": "origin/main", "linkedWorktree": false },
"sync": { "ahead": 1, "behind": 1, "fetchAgeSeconds": 255600, "fetchedNow": false },
"remote": { "normalized": "github.com/acme/widget", "host": "github.com", "publicForge": true },
"identity":{ "email": "you@example.com", "source": "global", "repositoryOverride": false },
"workingTree": { "staged": [], "unstaged": ["package.json"], "untracked": [], "clean": false },
"duplicateCheckouts": [ { "path": "/elsewhere/widget", "branch": "main", "ahead": 3, "aheadOfThisOne": true } ],
"blockers": [
"diverged from origin/main by 1 commit(s) (and 1 ahead); you are reading code the remote has already moved past, and a push will be rejected",
"another checkout of the same remote is ahead of this one: /elsewhere/widget (main, 3 ahead vs 1); you may be editing the stale copy"
],
"warnings": [
"remote refs were last updated 71h ago; ahead/behind is computed from that snapshot, not from the remote as it is now (re-run with fetch: true)"
],
"verdict": "blocked",
"safeToWrite": false
}
safeToWrite is the one field a caller needs. blockers are states where
proceeding loses or misplaces work; warnings are things worth knowing that
are not automatically wrong.
| Parameter | Effect |
|---|---|
path (required) | Absolute path to the working copy, or any directory inside it |
fetch | Contact the remote first so ahead/behind is current. The only networked option, default false |
expectBranch | The branch you believe you are on. A mismatch is a blocker |
expectIdentity | The email you expect to author with. A mismatch is a blocker |
remote | Remote to compare against (default origin) |
staleAfterHours | When remote refs count as stale (default 24) |
findDuplicates / duplicateRoots / maxDepth / maxRepos | Control the search for other clones |
expectBranch and expectIdentity are how you turn a report into an assertion.
Without them the plugin describes; with them it refuses.
checkout_guard_scan — every working copy under a root
{ "roots": ["/abs/path/to/projects"], "maxDepth": 3 }
Returns one row per checkout — branch, upstream, ahead/behind, uncommitted counts, fetch age — plus:
needsAttention: the ones that are behind, diverged, detached, or have no upstreamduplicateRemotes: paths grouped by remote, so two clones of one repository are visible at a glance
Run this first to find out which checkouts deserve a closer look, then hand
those paths to checkout_guard.
What it deliberately does not do
- It does not tell you a remote is archived. That needs the forge's API, and
this plugin does not make authenticated HTTP calls.
remote.normalizedgives youhost/owner/repoto check yourself. - It does not fix anything. No pull, no rebase, no stash, no checkout. The recovery for "behind" and the recovery for "diverged" are different, and picking between them is not a decision a guard should make for you.
- It does not judge your identity. It reports which config your author line
comes from, and warns only when a public forge is involved and the repository
has no local override. If you pass
expectIdentity, it compares against exactly what you named — it never guesses what your address should be.
Safety properties
- No writes to any repository. The only command that can modify anything is
git fetch --dry-run, which runs only whenfetch: trueand touches nothing butFETCH_HEAD. GIT_OPTIONAL_LOCKS=0on every call. Ordinarygit statusrefreshes and rewrites the index, taking.git/index.lock. A guard whose whole premise is "another agent may be in this checkout right now" must not be able to block that agent.GIT_TERMINAL_PROMPT=0, emptyGIT_ASKPASS. A fetch against a private remote fails fast instead of parking on a credential prompt nobody can see.- Credentials are stripped during URL normalization, before a remote can reach a result object or a log line.
- Path allowlist. Both sides are
realpath'd, so a symlink planted inside an allowed root cannot read outside it.
Configuration
- id: checkout-guard
name: 'dsh-checkout-guard'
config:
roots:
- /Users/you/projects
roots bounds everything the plugin will look at. Unset, it defaults to your
home directory.
Development
npm install
npm test # unit tests against real git repos in a temp dir
npm run test:boot # loads the plugin into a real cordis Context
There is no build step. src/index.js is the published entry point, byte
for byte. Plugins that compile to lib/ and commit the output have to keep the
two in sync forever, and every test that imports src/ passes while the
artifact an installer runs is stale. Removing the build removes the whole class.
The unit tests create real repositories — real remotes, real pushes, real divergence — rather than stubbing git. A hand-written double answers the way its author expects, and the author's expectation about repository state is precisely what is under test.
tests/boot.test.mjs exits 0 with a SKIP from a bare clone so contributors are
not blocked, and exits 1 under DSH_BOOT_STRICT=1, which CI sets. A skipped
integration test reported as green is worse than no test at all.
中文 · MIT
프로젝트 파일 및 신호
표시된 항목은 디렉터리 스냅샷에서 감지된 공개 저장소 신호입니다.
저장소 정보
- 언어
- JavaScript
- 라이선스
- MIT
- 마지막 업데이트
- 2026. 8. 18. AM 3:26
신중하게 설치하기
소스 코드, 권한, 수명 주기 스크립트, 의존성 및 네트워크 접근을 검토하고 신뢰하지 않는 플러그인은 격리 환경에서 테스트하세요.