Skip to content

CapCut 7.x edits are discarded when the active draft lives under Timelines/ #50

Description

@jaecho1001

Bug

A real CapCut 7.7 project stores its active document under Timelines/<main_timeline_id>/draft_info.json. The project-root draft_info.json is a derived mirror that CapCut regenerates from the nested timeline when the project opens.

The current draft-store discovery only considers root candidates. A write therefore reports success and changes the root mirror, but CapCut discards the edit on the next open.

Reproduction

With a CapCut 7.7 project whose text segment id is known:

capcut set-text "/path/to/project" <caption-segment-id> "updated caption"

Observed against a real project:

  1. The CLI returns success and the root draft_info.json contains the new text.
  2. Timelines/<active-id>/draft_info.json still contains the old text.
  3. Opening the project shows the old caption.
  4. CapCut rewrites the root mirror, removing the CLI edit.

The relevant structural pointer is:

{
  "id": "<timeline-id>",
  "main_timeline_id": "<timeline-id>"
}

in Timelines/project.json.

Expected

For verified pre-8.7 CapCut storage:

  • Directory discovery follows main_timeline_id (falling back to id) to the active nested draft_info.json, or nested draft_content.json when needed.
  • Normal writes keep the active timeline and readable root mirrors synchronized through the existing transactional write path.
  • sync-timelines repairs in the structurally safe nested-active → root-mirror direction.
  • Existing CapCut 8.7+ template-2.tmp priority remains unchanged.
  • Explicit root or archived-timeline paths cannot overwrite the selected active timeline.
  • Malformed paths and symlink escapes fall back safely.

This is distinct from #35/#39, which cover the CapCut 8.7+ root mirror/template layout.

Activity

  1. renezander030 commented on Aug 2, 2026

    @renezander030
    Owner

    Hi @jaecho1001, thanks for the report and for the PR that goes with it.

    The layout you describe is genuinely a new axis for this repo. #35/#39 covered the CapCut 8.7+ root mirror and template-2.tmp, and the store currently knows three shapes: draft_content.json primary, draft_info.json primary (newer Mac builds), and mirror-only. A nested Timelines/<main_timeline_id>/draft_info.json acting as the live document is not modeled anywhere, so if it holds, it is a real gap.

    Before I change which file the CLI treats as canonical, I need to be able to tell an active timeline apart from an archived one. Today a pre-8.7 project is served correctly from the root file, and that is the majority of users, so a wrong read here turns a silent no-op into propagated stale data.

    Could you attach the following from the project you reproduced on:

    1. Timelines/project.json, with ids left intact and any file paths or titles redacted as you see fit.
    2. The file tree: find "<project>" -name 'draft_*.json' -o -name 'template-2.tmp' -o -name 'project.json'
    3. The exact version string CapCut wrote, from draft_meta_info.json (or draft_content.json if that is where it lands for you), plus the OS.
    4. The before/after that shows the discard: content of Timelines/<active-id>/draft_info.json immediately after a capcut set-text run that reported success, showing it still carries the old text while the root file carries the new one.

    Point 4 is the one that decides it. If the nested file is stale after a successful write and CapCut then opens the old caption, the pointer is authoritative and your approach is right. If the nested file is simply an older snapshot that CapCut does not read, following the pointer would be the regression rather than the fix.

    Is the project a CapCut 7.7 install you can still reproduce on, or a draft you received from someone else? That changes how much the version number tells us.

    Rene

  2. k12ktv commented on Aug 23, 2026

    @k12ktv

    Same layout, different app generation: this reproduces on CapCut International for Mac 9.2.8, and there the nested file is authoritative in the way this issue describes. Posting it here rather than opening a new issue since it is the same axis. Note that this differs from the 8.5.0 observation in #60 / #68, where the nested layout did not discard tool writes.

    Environment

    • capcut-cli 0.20.0, Node 24.15.0
    • macOS 26.3.1, Apple Silicon
    • CapCut International for Mac, Info.plist CFBundleShortVersionString 9.2.0
    • draft last_modified_platform.app_version 9.2.8-beta4, new_version field 183.0.0, schema int 360000
    • app-created drafts contain no draft_content.json; draft_info.json is the only primary

    Reproduction

    1. Create an empty project in the app, then quit the app.
    2. capcut quickstart test --template <empty-project> --video x.mp4 --srt x.srt --force-write
    3. Lint is clean, the app lists the project, and the project opens without error.
    4. The timeline is empty — no video track, no text track.

    What the app actually reads

    App-created drafts carry Timelines/project.json with a main_timeline_id. The document CapCut 9.2.8 loads is Timelines/<main_timeline_id>/draft_info.json, mirrored in Timelines/<main_timeline_id>/template-2.tmp in the same directory. The project-root draft_info.json and template-2.tmp behave as legacy mirrors: after a successful CLI write they carry the new content while the nested file still carries the old (empty) timeline, and the app opens the nested one. That is the before/after in point 4 of the maintainer's list, at 9.2.8 rather than 7.7.

    The CLI writes only the two root-level files, so every write is a silent no-op from the app's point of view.

    Workaround, verified by opening in the app

    Copy the CLI-written root draft_info.json over both Timelines/<main_timeline_id>/draft_info.json and Timelines/<main_timeline_id>/template-2.tmp, setting the JSON id to the timeline id. After that the video track and the text track both appear as written.

    Unrelated smaller finding

    editorProcesses() reported "CapCut is running" with no CapCut process present — ps -axo comm= | grep -i capcut was empty, and calling the function directly from node returned []. quickstart therefore refused to write without --force-write. Not reproduced deterministically, so mentioning it only in case it is a known race; happy to split it out if it is news.

    Fixture

    Happy to provide a capcut fixture bundle from this layout if it is useful for the 9.x case, but only once the bundle is confirmed to scrub device identifiers (#59).

  3. renezander030 commented on Aug 23, 2026

    @renezander030
    Owner

    Thanks @k12ktv. One part of this I could confirm from the code without any artifact, and it's now fixed; the rest I can't move on yet.

    The gap you point at is real, though not quite where the issue text puts it. The write path's silence on >= 8.7 is deliberate — CapCut creates Timelines/<id>/draft_info.json routinely on modern builds (#60 caught it doing so on an 8.5.0 open), so warning on every write would fire for the majority about a hazard nobody has evidenced. That stays.

    What was actually wrong is narrower and worse. diagnose already attaches the full redacted nested_evidence block on a >= 8.7 store, and fixture already bundles Timelines/project.json and the nested documents — both keyed off nestedTimelines.length. But the diagnose next_action and the version support note were keyed off the layout value, which stops at 8.7 by design. So a 9.x user got a report carrying nested-Timelines evidence with no line of prose anywhere explaining what the structure was or that a bundle of it is the thing this issue has been waiting on. That's #95, fixed in #96 with a claim-free note — it asserts neither your discard finding nor #68's survival finding, because both predate the 8.7 storage change and neither transfers across it.

    What I still can't do is flip which file is canonical. That's PR #51's change, rejected once already pending a field artifact, and it repoints reads and writes for every store matching the layout.

    Your stated blocker is gone. #59 was fixed in #66 and merged on 2026-08-09 — device_id, mac_address and hard_disk_id are redacted, and SANITIZE_REPORT.json no longer writes raw paths. That shipped in 0.18.0, so the 0.20.0 you're running already has it. capcut fixture <project> --out <dir> on the 9.2.8 project is unblocked today, and with #96 the tool will now tell you so.

    Two things I'd want pinned down before treating 9.2.8 as a confirmed second sighting:

    1. editorProcesses() — I can't reproduce the shape you describe. src/store.ts is a stateless spawnSync("ps", ["-axo", "comm="]) plus a substring filter, and its only caller in src/draft.ts is gated on running.length > 0. If ps was empty and a direct call returned [], that message has no path to fire. Could you paste the exact stderr line? If it was the version-boundary refusal rather than the process check, that's a different bug and worth its own issue.

    2. The version string. You report CFBundleShortVersionString 9.2.0, app_version 9.2.8-beta4, and title the report 9.2.8. Which does capcut version <project> print for the store, and is this a public build or a beta channel? It changes how the registry ceiling should treat it.

    Rene

  4. IrizaD commented on Aug 31, 2026

    @IrizaD

    Data point from CapCut 8.4.0-beta6 on macOS (capcut-cli 0.21.1) — this layout behaves differently from the 7.x behaviour described here, so it may help bound the fix.

    What happened

    1. Created a draft with capcut init <name> --template <copy-of-a-real-8.4-draft>. (The bundled 6.5.0 template produces a draft that 8.4 refuses to open — issue init: bundled template stamps version 7 / empty new_version, and CapCut 8.5.0 refuses to open the draft #67. Using a copy of an existing app-created draft as the template sidesteps it and yields a clean empty project with the correct schema.)
    2. add-video + add-text on the CLI-written root files. At this point sync-timelines reported layout: info-primary, nested_available: 0 — no Timelines/ directory existed, and draft_info.json + template-2.tmp were in sync.
    3. Opened the project in CapCut 8.4.

    Result: edits survived

    CapCut opened the project correctly with both segments intact, and the app itself then created the nested layout — Timelines/project.json plus Timelines/<guid>/ with a full document set and .bak files.

    Re-reading after the app closed still shows the text segment:

    ID        Start   -End       Text
    db9ab327   0:00.20- 0:01.80  Prueba capcut-cli
    

    So on 8.4/macOS the direction appears to be root → nested (the app materializes the nested documents from the root files on first open), rather than nested being the pre-existing source of truth that overwrites root. No sync-timelines --nested was needed.

    Fixture

    Redacted bundle (capcut fixture, timeline JSON only, no media; 21 redactions — device IDs and macOS user): GIST_URL_HERE

    Happy to run any additional check on this machine if it helps narrow the version boundary.

  5. renezander030 commented on Aug 31, 2026

    @renezander030
    Owner

    Thanks @IrizaD — and the 8.4 band was genuinely thin, so this is welcome. Three things, one of which is a blocker on my side.

    The fixture link didn't make it. The comment carries the literal placeholder GIST_URL_HERE. A redacted bundle from an 8.4 store is the one artifact class this issue has been short of, so if you can paste the real URL I'll take it.

    Where I have to disagree on what the run shows. By your own step 2, sync-timelines reported nested_available: 0 and no Timelines/ directory existed when the CLI wrote. This issue is about a pre-existing nested document that the app reads while the CLI writes root — the stale-read case. In your run there was nothing nested to be stale: root was the only store, you wrote it, and the app then materialized the nested set from it. Survival was the only outcome available, so the run doesn't bear on the discard hypothesis either way. That's not a criticism of the report — the layout you describe is real and correctly observed. It's that the ordering makes it a different experiment.

    Your project is now the right test bed, though. It has an app-created nested layout sitting on disk. That is exactly the state the discard claim is about, and you can test it cheaply:

    1. capcut set-text <project> <segment-id> "changed on 8.4" against the project as it stands now (post-open, nested present).
    2. Without opening the app, diff root draft_info.json against Timelines/<main_timeline_id>/draft_info.json — I expect root new, nested old.
    3. Open in CapCut 8.4. Does the caption read the new text or the old one?

    Step 3 is the whole question. New text → 8.4 regenerates nested from root and behaves like the 8.5.0 finding in #68. Old text → 8.4 is a discard build and this issue reaches lower than 7.x. Either answer is worth having.

    What I'm not doing on this evidence. The CLI carries a version boundary at 8.5.0 (from #68) above which it tells users a root write will survive, because the 8.5.0 evidence was an open/close round trip on an existing nested document that came back byte-identical. Your report shows the app creating nested from root, which is a weaker claim than regenerating it over existing content. I'm not moving that boundary down to 8.4 until step 3 above says I can — telling an 8.4 user with a real nested layout that their edit is safe, on evidence that didn't cover that case, is the failure mode I'm trying to avoid.

    On #67: the bundled template still declares 6.5.0 and the fix there was a warning plus the documented --template <real draft> escape hatch, not a re-stamped template — so you hit expected behaviour and reached for the documented workaround independently. If the warning did not print when you ran init, tell me, because that part is supposed to be automatic.

    Rene

  6. renezander030 commented on Aug 31, 2026

    @renezander030
    Owner

    @k12ktv — three updates, and I owe you a correction on one of them.

    Your editorProcesses() finding was real, and I was wrong to doubt it. I said the message "has no path to fire" given an empty ps and a direct call returning []. It had a path, and #99 found it independently: editorProcesses() substring-matched the entire ps -axo comm= listing rather than comparing process names. Under npx / npm exec, npm rewrites its own process title to the full command line, which contains capcut-cli — so the tool detected itself and refused with "CapCut is running". Your sample at rest was empty because the offending process only exists during the run, which is exactly why it didn't reproduce the way either of us checked it. That's fixed in #101, on master now. If you were on the npx path, that fully explains what you saw, and your instinct to report it was right.

    The tool no longer contradicts your report on 9.2.8. #102/#103 just merged: versionTuple was dropping pre-release markers, so 9.2.8-beta4 compared equal to a 9.2.8 release and inherited the "your root edit should survive the next open" reassurance derived from a release build in #68. A pre-release now gets its own wording that asserts neither survival nor discard, and names the gap. Your store was being told the opposite of what you observed; it isn't any more.

    Which also makes my earlier question actionable. I asked whether 9.2.8-beta4 was a public build or a beta channel, because it changes how the registry ceiling treats it. At the time the code couldn't have acted on your answer even if you'd given it — the distinction was flattened before it reached any gate. It can now, so the question is worth re-asking rather than being trivia.

    What's still outstanding is the fixture. Your #59 blocker was cleared in #66 and shipped in 0.18.0 — device_id, mac_address and hard_disk_id are redacted and SANITIZE_REPORT.json no longer writes raw paths — so capcut fixture <project> --out <dir> on the 9.2.8 project is safe to run today.

    Yours is still the strongest report on this issue: it describes the decisive before/after in prose, with a mechanism and a workaround you verified by opening the app. What's missing is the artifact, not the argument. A bundle from that project is what unblocks repointing which file the CLI treats as canonical, and I'm not making that change on prose alone — for anyone, including you.

    If the 9.2.8 project is gone, say so and I'll stop asking; that's useful information too.

    Rene

  7. renezander030 commented on Sep 11, 2026

    @renezander030
    Owner

    Thanks @k12ktv and @IrizaD — two things in v0.23.0 bear on the runs described here, without touching the canonical question.

    init --template <dir> no longer copies the donor's Timelines/ (nor its .bak files or sidecar). Step 2 of the 9.2.8 reproduction — quickstart test --template <empty-project> — created the new draft with the donor's nested, empty timeline document already in place, and Timelines/project.json pointing at it: exactly the file this issue says the app reads. With 0.23.0 the new draft ships root documents only, so the same steps are a genuinely different experiment on 9.2.8: if the app materialises Timelines/ from the root documents the way 8.4.0 did in @IrizaD's run, the timeline shows up; if the draft still opens empty, that is the strongest discard evidence so far.

    Without --template, the CLI now seeds new drafts from the store's newest app-authored project (markers and settings, none of the content), so the init --template <copy-of-a-real-draft> workaround from the 8.4.0 report is the default — #67 / #111.

    The canonical flip stays gated on the artifact requested above: the nested draft_info.json still holding the old text right after a set-text that reported success, on a store where the nested document pre-exists. capcut fixture <project> --out <dir> produces it redacted.

  8. fpisasale commented on Oct 2, 2026

    @fpisasale

    Evidence from CapCut 8.7.0 (Windows): the app reads Timelines/<id>/draft_content.json, and overwrites the root files with it on close

    Environment: CapCut desktop 8.7.0.3685, Windows 11, capcut-cli 0.26.0. The project was created in the app (9:16 canvas, one 4K clip, one text segment), and then closed.

    Method. With the app fully closed, we wrote a different sentinel text into the same text material of each timeline copy. Each write was a single atomic rename, and compact JSON with ensure_ascii=False round-trips byte-identical:

    file sentinel
    draft_content.json (root) COPIA RADICE
    Timelines/<id>/draft_content.json COPIA TIMELINES
    template-2.tmp (root) COPIA TEMPLATE
    draft_content.json.bak left untouched (SOTTOTITOLO)

    Result 1: on open, the app shows COPIA TIMELINES. The nested document is the one CapCut 8.7 loads. Opening alone changed only draft_settings and created a .locked file in the project folder. It did not touch the timeline copies or root_meta_info.json.

    Result 2: on close, the nested document wins everywhere. After closing the project:

    • root draft_content.json and root template-2.tmp were both rewritten with the nested content (COPIA TIMELINES);
    • Timelines/<id>/template-2.tmp and Timelines/<id>/draft_content.json.bak were created;
    • .locked was removed and root_meta_info.json was updated.

    So on 8.7 a root-only CLI edit is not just ignored on open: it is discarded on the next close. This is the 8.7 counterpart of the 7.x behaviour described in this issue.

    Practical consequence for capcut-cli users on 8.7: every write must be followed by capcut sync-timelines <project> --nested --apply. In our pipeline we now treat that step as mandatory.

    Two side findings that may help:

    • .locked is a per-project "open in editor" marker: it exists only while that project is open. It is finer-grained than detecting CapCut.exe (the process stays alive on the home screen).
    • Drafts built by quickstart / compile have no Timelines/ folder. We have not yet checked what the app does with them on open and close.

    Sanitized capcut fixture bundle (files flattened, / → __): https://gist.github.com/fpisasale/8b344dccd4f4a731a35ef49ca55f0ff0 — the bundle of the project, taken after the close round-trip and one more root-only add-text ("SOLO RADICE"). It shows the root/nested divergence that diagnose reports. Note: the bundle needed one manual redaction, which we are reporting separately.

    Thanks for capcut-cli: it is the most solid tool we found for this.

    — Spirito Digitale

  9. renezander030 commented on Oct 3, 2026

    @renezander030
    Owner

    Thanks @fpisasale — this supplies the fixture and the experiment on a pre-existing nested timeline that I was waiting for. I checked the bundle: the root copies contain the additional SOLO RADICE caption while the selected nested document does not.

    I’m using it for regression coverage and an active-timeline fix, initially scoped to the evidenced Windows 8.7.0 build. The implementation will follow the selected timeline rather than copy the root into every timeline directory. Once the draft PR is ready, an open/close check with the patched CLI on your machine would confirm the app behavior.

    I’m also addressing #133 and #134 in the same reviewable patch. The 7.x and Mac cases will remain separately unverified; this issue will stay open pending the patched Windows round-trip.

  10. renezander030 commented on Oct 3, 2026

    @renezander030
    Owner

    The patch is ready for review in draft PR #135: #135

    Your field fixture is committed with attribution. On the evidenced Windows 8.7.0 layout, the selected active document drives reads and normal writes synchronize it with its readable mirrors and the root copies. Other timelines, IDs, and envelopes are preserved; assets and metadata remain at the project root. sync-timelines follows active → root on this layout and keeps its newer-mirror gate.

    The full local suite passes (1,017 tests). This verifies file behavior, not the desktop round-trip. When you can test the PR, please edit a caption in a copy of your existing project, open it in CapCut 8.7.0.3685, close it, and confirm that the caption survives. An add-video and restore check would also help. The exact steps are in docs/reviews/issues-50-133-134.md in the PR.

    This issue stays open pending that confirmation; the 7.x and Mac reports remain separate evidence gaps.

  11. renezander030 commented on Oct 3, 2026

    @renezander030
    Owner

    The patch is merged and published as v0.26.1; npm's latest now points to it.

    npm install -g capcut-cli@0.26.1
    capcut --version

    The packed release passed a fresh-install check against your contributed active-timeline fixture, including caption edits, document ID preservation, and restore. All 1,017 tests and the Windows/macOS/Linux CI checks passed.

    The remaining check is the patched desktop round-trip on CapCut 8.7.0.3685: edit a caption in a backed-up copy of the existing project with CapCut closed, open and close it in the app, and confirm that the caption survives. The end-to-end guide includes the asset-import and restore checks. Review any old root-only edits shown as divergence before making a new edit, because the selected active timeline now drives writes on this layout.

    Keeping this issue open for that confirmation and the separate 7.x/Mac evidence gaps.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions